Changeset 0.22.1 (#95)
This commit is contained in:
26
CHANGELOG.md
26
CHANGELOG.md
@@ -2,6 +2,32 @@
|
|||||||
|
|
||||||
All notable changes to Chat Switchboard.
|
All notable changes to Chat Switchboard.
|
||||||
|
|
||||||
|
## [0.22.1] — 2026-03-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Provider profile schemas.** Each provider type declares its configurable settings with types, defaults, validation constraints, and dependency rules. Built-in schemas for openai, anthropic, venice, openrouter. Unknown types fall back to the openai schema. `providers/profile.go`.
|
||||||
|
- **Provider hooks (pre-request / post-stream).** Declarative request/response transforms driven by `provider_configs.settings` JSONB. Replaces any need for hardcoded provider switch statements in the completion path. `providers/hooks.go`.
|
||||||
|
- **OpenAI:** `system_prompt_prefix`, `frequency_penalty`, `presence_penalty` → injected into ExtraBody.
|
||||||
|
- **Anthropic:** `extended_thinking` + `thinking_budget` → injects `thinking` config into wire JSON, sets `anthropic-beta` header, clears temperature.
|
||||||
|
- **Venice:** `enable_thinking`, `enable_web_search`, `include_venice_system_prompt` → builds `venice_parameters` in ExtraBody.
|
||||||
|
- **OpenRouter:** `route`, `require_parameters` → serialized into `X-Provider-Preferences` header.
|
||||||
|
- **`ExtraBody` on `CompletionRequest`.** Provider hooks write arbitrary key-value pairs that get merged into the wire-format JSON via `mergeExtraBody()` in both OpenAI and Anthropic `doRequest` methods. Keeps the canonical request type clean while supporting provider-specific extensions.
|
||||||
|
- **Anthropic extended thinking stream support.** `content_block_delta` with `type=thinking_delta` now routes to the `Reasoning` field on `StreamEvent`, matching the existing `reasoning_content` path used by OpenAI-compatible providers.
|
||||||
|
- **Provider type registry.** `providers.RegisterType()` combines provider implementation + metadata (name, description, default endpoint, profile schema). `providers.ListTypes()` returns all registered types.
|
||||||
|
- **`GET /api/v1/admin/provider-types` endpoint.** Returns metadata and profile schemas for all registered provider types. Used by admin UI to render provider creation forms and show available settings per type.
|
||||||
|
- **Preset setting overrides.** `MergePresetSettings()` merges persona-level overrides onto provider-level settings, respecting `ProviderOnly` fields that cannot be changed at the preset level.
|
||||||
|
- **Comprehensive tests.** `providers/hooks_test.go`: 17 tests covering all four hook implementations, profile schemas, GetHooks, MergePresetSettings, mergeExtraBody, nil safety.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `providers/registry.go`: `Init()` now uses `RegisterType()` with full metadata instead of bare `Register()`. `ProviderTypeMeta` struct with ID, name, description, default endpoint, profile schema.
|
||||||
|
- `providers/provider.go`: `CompletionRequest.ExtraBody` field (json:"-"). `mergeExtraBody()` helper function. `Model` struct unchanged.
|
||||||
|
- `providers/openai.go`: `doRequest()` merges ExtraBody into wire JSON before HTTP dispatch.
|
||||||
|
- `providers/anthropic.go`: `doRequest()` merges ExtraBody + sets `anthropic-beta` header when thinking is enabled. Stream processing handles `thinking_delta` content block deltas. Wire type gains `Thinking` field.
|
||||||
|
- `handlers/completion.go`: `PreRequest` hook invoked at all three dispatch paths (multi-model, streaming, sync).
|
||||||
|
- `handlers/stream_loop.go`: `PostStreamEvent` hook invoked after each streaming event in both `streamWithToolLoop` and `streamModelResponse`.
|
||||||
|
- `handlers/health_admin.go`: Added `GetProviderTypes` handler.
|
||||||
|
- `main.go`: `GET /api/v1/admin/provider-types` route registered.
|
||||||
|
|
||||||
## [0.22.0] — 2026-03-02
|
## [0.22.0] — 2026-03-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -186,9 +186,13 @@ server/
|
|||||||
├── notelinks/ # Wikilink extraction (regex → NoteLink structs)
|
├── notelinks/ # Wikilink extraction (regex → NoteLink structs)
|
||||||
├── knowledge/ # KB chunking, embedding, search
|
├── knowledge/ # KB chunking, embedding, search
|
||||||
├── providers/ # LLM provider adapters
|
├── providers/ # LLM provider adapters
|
||||||
│ ├── provider.go # Provider interface
|
│ ├── provider.go # Provider interface + ExtraBody/mergeExtraBody
|
||||||
│ ├── anthropic.go
|
│ ├── registry.go # Data-driven type registry with metadata (v0.22.1)
|
||||||
│ ├── openai.go
|
│ ├── profile.go # Profile schemas per provider type (v0.22.1)
|
||||||
|
│ ├── hooks.go # PreRequest/PostStreamEvent transforms (v0.22.1)
|
||||||
|
│ ├── hooks_test.go # 17 tests: all hooks, schemas, merge, nil safety
|
||||||
|
│ ├── anthropic.go # Anthropic Messages API (+ extended thinking)
|
||||||
|
│ ├── openai.go # OpenAI-compatible (+ ExtraBody merge)
|
||||||
│ ├── openrouter.go
|
│ ├── openrouter.go
|
||||||
│ └── venice.go
|
│ └── venice.go
|
||||||
├── middleware/ # Auth, admin, CORS, rate limiting
|
├── middleware/ # Auth, admin, CORS, rate limiting
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ v0.21.7 Bugfix: scanJSON driver buffer aliasing ✅
|
|||||||
v0.22.0 Provider Health + Capability Overrides ✅
|
v0.22.0 Provider Health + Capability Overrides ✅
|
||||||
+ Workspace Pane Refactor (FE)
|
+ Workspace Pane Refactor (FE)
|
||||||
│
|
│
|
||||||
v0.22.1 Provider Extensions (declarative config)
|
v0.22.1 Provider Extensions (declarative config) ✅
|
||||||
│
|
│
|
||||||
v0.22.2 Routing Policies + Fallback Chains
|
v0.22.2 Routing Policies + Fallback Chains
|
||||||
│
|
│
|
||||||
@@ -905,7 +905,7 @@ Depends on: usage tracking (v0.10.0), capabilities resolver (v0.9.1).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v0.22.1 — Provider Extensions (Declarative Config)
|
## v0.22.1 — Provider Extensions (Declarative Config) ✅
|
||||||
|
|
||||||
Replace hardcoded provider-specific behavior with data-driven configuration.
|
Replace hardcoded provider-specific behavior with data-driven configuration.
|
||||||
Same model, different provider → different parameters.
|
Same model, different provider → different parameters.
|
||||||
@@ -913,23 +913,30 @@ Same model, different provider → different parameters.
|
|||||||
Depends on: provider health (v0.22.0).
|
Depends on: provider health (v0.22.0).
|
||||||
|
|
||||||
**Provider Profiles**
|
**Provider Profiles**
|
||||||
- [ ] `provider_profiles` JSONB column on `provider_configs` table (or separate table if schema cleaner)
|
- [x] Profile schema per provider type: defines available settings, types, defaults, validation, dependency rules (`providers/profile.go`)
|
||||||
- [ ] Profile schema per provider type: defines available settings, types, defaults, validation
|
- [x] Built-in profile schemas for: openai, anthropic, venice, openrouter (unknown types fall back to openai-compatible)
|
||||||
- [ ] Built-in profile schemas for: openai, anthropic, venice, openrouter (others inherit openai-compatible)
|
- [x] System prompt injection: `system_prompt_prefix` setting prepended to first system message (all providers)
|
||||||
- [ ] System prompt injection: provider-specific preambles (e.g. Venice character system prompt)
|
- [x] Thinking mode toggle: Anthropic `extended_thinking` + `thinking_budget` → injects `thinking.type=enabled`; Venice `enable_thinking` → disables Venice system prompt
|
||||||
- [ ] Thinking mode toggle: per-provider `think` parameter mapping (Venice `venice_parameters.include_venice_system_prompt`, Anthropic `thinking.type=enabled`, etc.)
|
- [ ] _(not needed)_ `provider_profiles` JSONB column — existing `provider_configs.settings` JSONB already serves this purpose
|
||||||
|
|
||||||
**Request/Response Transforms**
|
**Request/Response Transforms**
|
||||||
- [ ] `PreRequestHook(providerType string, profile JSONB, req *CompletionRequest)` — decorate request before dispatch
|
- [x] `Hooks.PreRequest(cfg, req)` — decorate request before dispatch (system prompt prefix, ExtraBody injection)
|
||||||
- [ ] `PostResponseHook(providerType string, profile JSONB, resp *StreamEvent)` — normalize provider-specific response fields
|
- [x] `Hooks.PostStreamEvent(cfg, event)` — normalize provider-specific response fields (Venice thinking extraction)
|
||||||
- [ ] Hook implementations: Venice thinking extraction, Anthropic extended thinking, OpenRouter model routing headers
|
- [x] Hook implementations: Venice web search + thinking, Anthropic extended thinking + beta header, OpenRouter routing preferences + require_parameters, OpenAI frequency/presence penalty
|
||||||
- [ ] Preset-level overrides: Persona can override provider-specific settings ("always enable thinking for this Persona")
|
- [x] Preset-level overrides: `MergePresetSettings()` merges persona overrides onto provider settings, respecting `ProviderOnly` fields
|
||||||
- [ ] `ProviderConfig.Settings` fully utilized: hooks read from config, not hardcoded switch statements
|
- [x] `ProviderConfig.Settings` fully utilized: hooks read from config, not hardcoded switch statements
|
||||||
|
- [x] `CompletionRequest.ExtraBody` field + `mergeExtraBody()` for provider-specific wire fields
|
||||||
|
- [x] Anthropic extended thinking stream support: `thinking_delta` content blocks → `Reasoning` field
|
||||||
|
|
||||||
**Provider Type Registry** (prep for v0.22.2)
|
**Provider Type Registry**
|
||||||
- [ ] `providers.Init()` reads provider type → adapter mapping from registry instead of hardcoded list
|
- [x] `providers.Init()` uses `RegisterType()` with full metadata (name, description, default endpoint, profile schema)
|
||||||
- [ ] New provider types registrable via config (openai-compatible endpoint + custom profile schema)
|
- [x] `ProviderTypeMeta` struct: ID, name, description, default_endpoint, profile_schema
|
||||||
- [ ] `GET /api/v1/admin/provider-types` endpoint: list available provider types with their profile schemas
|
- [x] `GET /api/v1/admin/provider-types` endpoint: list available provider types with their profile schemas
|
||||||
|
- [ ] _(deferred → v0.22.3)_ New provider types registrable via config file (openai-compatible endpoint + custom profile schema)
|
||||||
|
|
||||||
|
**Search Provider Health** (carried from v0.22.0)
|
||||||
|
- [ ] _(deferred → v0.22.2)_ `RecordOutcome` for web_search and url_fetch tool calls
|
||||||
|
- [ ] _(deferred → v0.22.2)_ Rate limit tracking per provider config
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
|
|||||||
// ── Preset unwrap: preset overrides defaults, explicit request fields win ──
|
// ── Preset unwrap: preset overrides defaults, explicit request fields win ──
|
||||||
var presetSystemPrompt string
|
var presetSystemPrompt string
|
||||||
var personaID string // tracks active persona for KB scoping
|
var personaID string // tracks active persona for KB scoping
|
||||||
|
var presetThinkingBudget *int // persona-level thinking budget for hook injection (v0.22.1)
|
||||||
if req.PresetID != "" {
|
if req.PresetID != "" {
|
||||||
preset := ResolvePreset(h.stores, req.PresetID, userID)
|
preset := ResolvePreset(h.stores, req.PresetID, userID)
|
||||||
if preset == nil {
|
if preset == nil {
|
||||||
@@ -136,6 +137,7 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
|
|||||||
if preset.SystemPrompt != "" {
|
if preset.SystemPrompt != "" {
|
||||||
presetSystemPrompt = preset.SystemPrompt
|
presetSystemPrompt = preset.SystemPrompt
|
||||||
}
|
}
|
||||||
|
presetThinkingBudget = preset.ThinkingBudget
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Project persona fallback (v0.19.2): if no explicit preset, check project ──
|
// ── Project persona fallback (v0.19.2): if no explicit preset, check project ──
|
||||||
@@ -161,6 +163,7 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
|
|||||||
if preset.SystemPrompt != "" {
|
if preset.SystemPrompt != "" {
|
||||||
presetSystemPrompt = preset.SystemPrompt
|
presetSystemPrompt = preset.SystemPrompt
|
||||||
}
|
}
|
||||||
|
presetThinkingBudget = preset.ThinkingBudget
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,6 +177,17 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Inject persona-level thinking budget into provider settings (v0.22.1) ──
|
||||||
|
// When a persona specifies a thinking budget, promote it into provider
|
||||||
|
// settings so hooks can activate extended thinking automatically.
|
||||||
|
if presetThinkingBudget != nil && *presetThinkingBudget > 0 {
|
||||||
|
if providerCfg.Settings == nil {
|
||||||
|
providerCfg.Settings = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
providerCfg.Settings["extended_thinking"] = true
|
||||||
|
providerCfg.Settings["thinking_budget"] = *presetThinkingBudget
|
||||||
|
}
|
||||||
|
|
||||||
// ── Team policy: require_private_providers ──
|
// ── Team policy: require_private_providers ──
|
||||||
if err := enforcePrivateProviderPolicy(userID, configID); err != nil {
|
if err := enforcePrivateProviderPolicy(userID, configID); err != nil {
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||||
@@ -284,9 +298,9 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if stream {
|
if stream {
|
||||||
h.streamCompletion(c, provider, providerCfg, provReq, channelID, userID, model, configID, providerScope, personaID, workspaceID)
|
h.streamCompletion(c, provider, providerCfg, provReq, channelID, userID, model, providerID, configID, providerScope, personaID, workspaceID)
|
||||||
} else {
|
} else {
|
||||||
h.syncCompletion(c, provider, providerCfg, provReq, channelID, userID, model, configID, providerScope, personaID, workspaceID)
|
h.syncCompletion(c, provider, providerCfg, provReq, channelID, userID, model, providerID, configID, providerScope, personaID, workspaceID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,8 +399,13 @@ func (h *CompletionHandler) multiModelStream(
|
|||||||
provReq.Tools = h.buildToolDefs(c.Request.Context(), userID, hasBrowserTools, req.DisabledTools, workspaceID)
|
provReq.Tools = h.buildToolDefs(c.Request.Context(), userID, hasBrowserTools, req.DisabledTools, workspaceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply provider-specific request hooks (v0.22.1)
|
||||||
|
if hooks := providers.GetHooks(providerID); hooks != nil {
|
||||||
|
hooks.PreRequest(providerCfg, &provReq)
|
||||||
|
}
|
||||||
|
|
||||||
// Stream this model's response using the shared streaming core
|
// Stream this model's response using the shared streaming core
|
||||||
result := streamModelResponse(c, provider, providerCfg, &provReq, model, target.DisplayName, userID, channelID, personaID, workspaceID, configID, h.hub, h.health)
|
result := streamModelResponse(c, provider, providerCfg, &provReq, model, providerID, target.DisplayName, userID, channelID, personaID, workspaceID, configID, h.hub, h.health)
|
||||||
|
|
||||||
// Persist assistant message with model attribution
|
// Persist assistant message with model attribution
|
||||||
if result.Content != "" {
|
if result.Content != "" {
|
||||||
@@ -549,9 +568,14 @@ func (h *CompletionHandler) streamCompletion(
|
|||||||
provider providers.Provider,
|
provider providers.Provider,
|
||||||
cfg providers.ProviderConfig,
|
cfg providers.ProviderConfig,
|
||||||
req providers.CompletionRequest,
|
req providers.CompletionRequest,
|
||||||
channelID, userID, model, configID, providerScope, personaID, workspaceID string,
|
channelID, userID, model, providerID, configID, providerScope, personaID, workspaceID string,
|
||||||
) {
|
) {
|
||||||
result := streamWithToolLoop(c, provider, cfg, &req, model, userID, channelID, personaID, workspaceID, configID, h.hub, h.health)
|
// Apply provider-specific request hooks (v0.22.1)
|
||||||
|
if hooks := providers.GetHooks(providerID); hooks != nil {
|
||||||
|
hooks.PreRequest(cfg, &req)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := streamWithToolLoop(c, provider, cfg, &req, model, providerID, userID, channelID, personaID, workspaceID, configID, h.hub, h.health)
|
||||||
|
|
||||||
// Persist assistant response
|
// Persist assistant response
|
||||||
if result.Content != "" {
|
if result.Content != "" {
|
||||||
@@ -573,8 +597,13 @@ func (h *CompletionHandler) syncCompletion(
|
|||||||
provider providers.Provider,
|
provider providers.Provider,
|
||||||
cfg providers.ProviderConfig,
|
cfg providers.ProviderConfig,
|
||||||
req providers.CompletionRequest,
|
req providers.CompletionRequest,
|
||||||
channelID, userID, model, configID, providerScope, personaID, workspaceID string,
|
channelID, userID, model, providerID, configID, providerScope, personaID, workspaceID string,
|
||||||
) {
|
) {
|
||||||
|
// Apply provider-specific request hooks (v0.22.1)
|
||||||
|
if hooks := providers.GetHooks(providerID); hooks != nil {
|
||||||
|
hooks.PreRequest(cfg, &req)
|
||||||
|
}
|
||||||
|
|
||||||
var totalInput, totalOutput int
|
var totalInput, totalOutput int
|
||||||
var totalCacheCreation, totalCacheRead int
|
var totalCacheCreation, totalCacheRead int
|
||||||
var finalContent string
|
var finalContent string
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
capspkg "git.gobha.me/xcaliber/chat-switchboard/capabilities"
|
capspkg "git.gobha.me/xcaliber/chat-switchboard/capabilities"
|
||||||
"git.gobha.me/xcaliber/chat-switchboard/health"
|
"git.gobha.me/xcaliber/chat-switchboard/health"
|
||||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||||
|
"git.gobha.me/xcaliber/chat-switchboard/providers"
|
||||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -278,3 +279,13 @@ func buildSourceAnnotations(catalog *models.ModelCapabilities, heuristic *models
|
|||||||
}
|
}
|
||||||
return sources
|
return sources
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Provider Types Endpoint ─────────────────
|
||||||
|
|
||||||
|
// GetProviderTypes returns metadata and profile schemas for all registered
|
||||||
|
// provider types. Used by the admin UI to render provider creation forms
|
||||||
|
// and show available settings.
|
||||||
|
func GetProviderTypes(c *gin.Context) {
|
||||||
|
types := providers.ListTypes()
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": types})
|
||||||
|
}
|
||||||
|
|||||||
@@ -516,7 +516,12 @@ func (h *MessageHandler) Regenerate(c *gin.Context) {
|
|||||||
|
|
||||||
// ── Stream the response (shared loop handles tools, reasoning, SSE) ──
|
// ── Stream the response (shared loop handles tools, reasoning, SSE) ──
|
||||||
|
|
||||||
result := streamWithToolLoop(c, provider, providerCfg, &provReq, model, userID, channelID, personaID, workspaceID, configID, h.hub, comp.health)
|
// Apply provider-specific request hooks (v0.22.1)
|
||||||
|
if hooks := providers.GetHooks(providerID); hooks != nil {
|
||||||
|
hooks.PreRequest(providerCfg, &provReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := streamWithToolLoop(c, provider, providerCfg, &provReq, model, providerID, userID, channelID, personaID, workspaceID, configID, h.hub, comp.health)
|
||||||
|
|
||||||
// Persist as sibling (regen) with tool activity
|
// Persist as sibling (regen) with tool activity
|
||||||
if result.Content != "" {
|
if result.Content != "" {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func streamWithToolLoop(
|
|||||||
provider providers.Provider,
|
provider providers.Provider,
|
||||||
cfg providers.ProviderConfig,
|
cfg providers.ProviderConfig,
|
||||||
req *providers.CompletionRequest,
|
req *providers.CompletionRequest,
|
||||||
model, userID, channelID, personaID, workspaceID, configID string,
|
model, providerType, userID, channelID, personaID, workspaceID, configID string,
|
||||||
hub *events.Hub,
|
hub *events.Hub,
|
||||||
health HealthRecorder,
|
health HealthRecorder,
|
||||||
) streamResult {
|
) streamResult {
|
||||||
@@ -102,6 +102,11 @@ func streamWithToolLoop(
|
|||||||
var toolCalls []providers.ToolCall
|
var toolCalls []providers.ToolCall
|
||||||
|
|
||||||
for event := range ch {
|
for event := range ch {
|
||||||
|
// Apply provider-specific post-processing hooks (v0.22.1)
|
||||||
|
if hooks := providers.GetHooks(providerType); hooks != nil {
|
||||||
|
hooks.PostStreamEvent(cfg, &event)
|
||||||
|
}
|
||||||
|
|
||||||
if event.Error != nil {
|
if event.Error != nil {
|
||||||
recordHealthFn(health, configID, callStart, event.Error)
|
recordHealthFn(health, configID, callStart, event.Error)
|
||||||
sendSSE(fmt.Sprintf(`{"error":"%s"}`, escapeJSON(event.Error.Error())))
|
sendSSE(fmt.Sprintf(`{"error":"%s"}`, escapeJSON(event.Error.Error())))
|
||||||
@@ -287,7 +292,7 @@ func streamModelResponse(
|
|||||||
provider providers.Provider,
|
provider providers.Provider,
|
||||||
cfg providers.ProviderConfig,
|
cfg providers.ProviderConfig,
|
||||||
req *providers.CompletionRequest,
|
req *providers.CompletionRequest,
|
||||||
model, displayName, userID, channelID, personaID, workspaceID, configID string,
|
model, providerType, displayName, userID, channelID, personaID, workspaceID, configID string,
|
||||||
hub *events.Hub,
|
hub *events.Hub,
|
||||||
health HealthRecorder,
|
health HealthRecorder,
|
||||||
) streamResult {
|
) streamResult {
|
||||||
@@ -322,6 +327,11 @@ func streamModelResponse(
|
|||||||
var toolCalls []providers.ToolCall
|
var toolCalls []providers.ToolCall
|
||||||
|
|
||||||
for event := range ch {
|
for event := range ch {
|
||||||
|
// Apply provider-specific post-processing hooks (v0.22.1)
|
||||||
|
if hooks := providers.GetHooks(providerType); hooks != nil {
|
||||||
|
hooks.PostStreamEvent(cfg, &event)
|
||||||
|
}
|
||||||
|
|
||||||
if event.Error != nil {
|
if event.Error != nil {
|
||||||
recordHealthFn(health, configID, callStart, event.Error)
|
recordHealthFn(health, configID, callStart, event.Error)
|
||||||
sendSSE(fmt.Sprintf(`{"error":"%s"}`, escapeJSON(event.Error.Error())))
|
sendSSE(fmt.Sprintf(`{"error":"%s"}`, escapeJSON(event.Error.Error())))
|
||||||
|
|||||||
@@ -760,6 +760,9 @@ func main() {
|
|||||||
admin.PUT("/models/:id/capabilities", capAdm.SetModelCapability)
|
admin.PUT("/models/:id/capabilities", capAdm.SetModelCapability)
|
||||||
admin.DELETE("/models/:id/capabilities/:overrideId", capAdm.DeleteModelCapability)
|
admin.DELETE("/models/:id/capabilities/:overrideId", capAdm.DeleteModelCapability)
|
||||||
admin.GET("/capability-overrides", capAdm.ListAllOverrides)
|
admin.GET("/capability-overrides", capAdm.ListAllOverrides)
|
||||||
|
|
||||||
|
// Provider Types (admin — v0.22.1)
|
||||||
|
admin.GET("/provider-types", handlers.GetProviderTypes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,10 +130,15 @@ func (p *AnthropicProvider) StreamCompletion(ctx context.Context, cfg ProviderCo
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// thinking blocks (extended thinking, v0.22.1) — no action on start,
|
||||||
|
// deltas arrive as thinking_delta in content_block_delta
|
||||||
|
|
||||||
case "content_block_delta":
|
case "content_block_delta":
|
||||||
if event.Delta.Type == "text_delta" {
|
if event.Delta.Type == "text_delta" {
|
||||||
ch <- StreamEvent{Delta: event.Delta.Text}
|
ch <- StreamEvent{Delta: event.Delta.Text}
|
||||||
|
} else if event.Delta.Type == "thinking_delta" && event.Delta.Thinking != "" {
|
||||||
|
// Extended thinking content → Reasoning field
|
||||||
|
ch <- StreamEvent{Reasoning: event.Delta.Thinking}
|
||||||
} else if event.Delta.Type == "input_json_delta" && currentToolIdx >= 0 {
|
} else if event.Delta.Type == "input_json_delta" && currentToolIdx >= 0 {
|
||||||
toolCalls[currentToolIdx].Function.Arguments += event.Delta.PartialJSON
|
toolCalls[currentToolIdx].Function.Arguments += event.Delta.PartialJSON
|
||||||
}
|
}
|
||||||
@@ -343,6 +348,15 @@ func (p *AnthropicProvider) doRequest(ctx context.Context, cfg ProviderConfig, r
|
|||||||
return nil, fmt.Errorf("marshal request: %w", err)
|
return nil, fmt.Errorf("marshal request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge hook-supplied extra fields into wire JSON (v0.22.1)
|
||||||
|
// This is how extended thinking config gets injected.
|
||||||
|
if len(req.ExtraBody) > 0 {
|
||||||
|
body, err = mergeExtraBody(body, req.ExtraBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("merge extra body: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(body))
|
httpReq, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -351,6 +365,13 @@ func (p *AnthropicProvider) doRequest(ctx context.Context, cfg ProviderConfig, r
|
|||||||
httpReq.Header.Set("x-api-key", cfg.APIKey)
|
httpReq.Header.Set("x-api-key", cfg.APIKey)
|
||||||
httpReq.Header.Set("anthropic-version", anthropicAPIVersion)
|
httpReq.Header.Set("anthropic-version", anthropicAPIVersion)
|
||||||
|
|
||||||
|
// Extended thinking requires the output-128k beta header
|
||||||
|
if len(req.ExtraBody) > 0 {
|
||||||
|
if _, hasThinking := req.ExtraBody["thinking"]; hasThinking {
|
||||||
|
httpReq.Header.Set("anthropic-beta", "output-128k-2025-02-19")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(httpReq)
|
resp, err := http.DefaultClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("provider request: %w", err)
|
return nil, fmt.Errorf("provider request: %w", err)
|
||||||
@@ -458,6 +479,7 @@ type anthropicStreamEvent struct {
|
|||||||
Delta struct {
|
Delta struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
|
Thinking string `json:"thinking"` // extended thinking delta (v0.22.1)
|
||||||
StopReason string `json:"stop_reason"`
|
StopReason string `json:"stop_reason"`
|
||||||
PartialJSON string `json:"partial_json"`
|
PartialJSON string `json:"partial_json"`
|
||||||
} `json:"delta"`
|
} `json:"delta"`
|
||||||
|
|||||||
296
server/providers/hooks.go
Normal file
296
server/providers/hooks.go
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hooks defines provider-specific request/response transforms.
|
||||||
|
// Implementations read from ProviderConfig.Settings (populated from the
|
||||||
|
// provider_configs.settings JSONB column) to decorate requests and
|
||||||
|
// normalize responses. This replaces hardcoded provider switch statements.
|
||||||
|
type Hooks interface {
|
||||||
|
// PreRequest modifies the canonical request before dispatch.
|
||||||
|
// May inject system prompt prefixes, set ExtraBody fields,
|
||||||
|
// and adjust request parameters based on provider settings.
|
||||||
|
PreRequest(cfg ProviderConfig, req *CompletionRequest)
|
||||||
|
|
||||||
|
// PostStreamEvent normalizes a streaming event after the provider
|
||||||
|
// emits it. For example, extracting thinking output from content.
|
||||||
|
PostStreamEvent(cfg ProviderConfig, event *StreamEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hook Registry ───────────────────────────
|
||||||
|
|
||||||
|
var hooks = map[string]Hooks{
|
||||||
|
"openai": &OpenAIHooks{},
|
||||||
|
"anthropic": &AnthropicHooks{},
|
||||||
|
"venice": &VeniceHooks{},
|
||||||
|
"openrouter": &OpenRouterHooks{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHooks returns the hooks for a provider type.
|
||||||
|
// Returns nil for unknown types (no transforms applied).
|
||||||
|
func GetHooks(providerType string) Hooks {
|
||||||
|
return hooks[providerType]
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterHooks adds or replaces hooks for a provider type.
|
||||||
|
func RegisterHooks(providerType string, h Hooks) {
|
||||||
|
hooks[providerType] = h
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Settings Helpers ────────────────────────
|
||||||
|
|
||||||
|
func settingString(s map[string]interface{}, key string) string {
|
||||||
|
if v, ok := s[key]; ok {
|
||||||
|
if str, ok := v.(string); ok {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func settingBool(s map[string]interface{}, key string) bool {
|
||||||
|
if v, ok := s[key]; ok {
|
||||||
|
switch b := v.(type) {
|
||||||
|
case bool:
|
||||||
|
return b
|
||||||
|
case string:
|
||||||
|
return b == "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func settingInt(s map[string]interface{}, key string, fallback int) int {
|
||||||
|
if v, ok := s[key]; ok {
|
||||||
|
switch n := v.(type) {
|
||||||
|
case float64:
|
||||||
|
return int(n) // JSON numbers decode as float64
|
||||||
|
case int:
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func settingFloat(s map[string]interface{}, key string) (float64, bool) {
|
||||||
|
if v, ok := s[key]; ok {
|
||||||
|
switch n := v.(type) {
|
||||||
|
case float64:
|
||||||
|
return n, true
|
||||||
|
case int:
|
||||||
|
return float64(n), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Common: System Prompt Prefix ────────────
|
||||||
|
|
||||||
|
// prependSystemPrompt injects a provider-level system prompt prefix
|
||||||
|
// into the first system message, or creates one if none exists.
|
||||||
|
func prependSystemPrompt(prefix string, req *CompletionRequest) {
|
||||||
|
if prefix == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, m := range req.Messages {
|
||||||
|
if m.Role == "system" {
|
||||||
|
req.Messages[i].Content = prefix + "\n\n" + m.Content
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No system message — insert at position 0
|
||||||
|
req.Messages = append([]Message{{Role: "system", Content: prefix}}, req.Messages...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── OpenAI Hooks ────────────────────────────
|
||||||
|
|
||||||
|
type OpenAIHooks struct{}
|
||||||
|
|
||||||
|
func (h *OpenAIHooks) PreRequest(cfg ProviderConfig, req *CompletionRequest) {
|
||||||
|
s := cfg.Settings
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prependSystemPrompt(settingString(s, "system_prompt_prefix"), req)
|
||||||
|
|
||||||
|
if req.ExtraBody == nil {
|
||||||
|
req.ExtraBody = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
if v, ok := settingFloat(s, "frequency_penalty"); ok {
|
||||||
|
req.ExtraBody["frequency_penalty"] = v
|
||||||
|
}
|
||||||
|
if v, ok := settingFloat(s, "presence_penalty"); ok {
|
||||||
|
req.ExtraBody["presence_penalty"] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *OpenAIHooks) PostStreamEvent(_ ProviderConfig, _ *StreamEvent) {
|
||||||
|
// No post-processing needed for vanilla OpenAI.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Anthropic Hooks ─────────────────────────
|
||||||
|
|
||||||
|
type AnthropicHooks struct{}
|
||||||
|
|
||||||
|
func (h *AnthropicHooks) PreRequest(cfg ProviderConfig, req *CompletionRequest) {
|
||||||
|
s := cfg.Settings
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prependSystemPrompt(settingString(s, "system_prompt_prefix"), req)
|
||||||
|
|
||||||
|
if settingBool(s, "extended_thinking") {
|
||||||
|
budget := settingInt(s, "thinking_budget", 10000)
|
||||||
|
if req.ExtraBody == nil {
|
||||||
|
req.ExtraBody = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
req.ExtraBody["thinking"] = map[string]interface{}{
|
||||||
|
"type": "enabled",
|
||||||
|
"budget_tokens": budget,
|
||||||
|
}
|
||||||
|
// Anthropic requires temperature=1 with extended thinking, and
|
||||||
|
// thinking is incompatible with temperature overrides.
|
||||||
|
req.Temperature = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnthropicHooks) PostStreamEvent(_ ProviderConfig, _ *StreamEvent) {
|
||||||
|
// Anthropic streaming already normalizes thinking → Reasoning field.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Venice Hooks ────────────────────────────
|
||||||
|
|
||||||
|
type VeniceHooks struct{}
|
||||||
|
|
||||||
|
func (h *VeniceHooks) PreRequest(cfg ProviderConfig, req *CompletionRequest) {
|
||||||
|
s := cfg.Settings
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prependSystemPrompt(settingString(s, "system_prompt_prefix"), req)
|
||||||
|
|
||||||
|
// Build venice_parameters from settings
|
||||||
|
veniceParams := map[string]interface{}{}
|
||||||
|
hasParams := false
|
||||||
|
|
||||||
|
if settingBool(s, "enable_web_search") {
|
||||||
|
veniceParams["enable_web_search"] = "always"
|
||||||
|
hasParams = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// include_venice_system_prompt defaults to true.
|
||||||
|
// Only set to false when explicitly configured.
|
||||||
|
if v, ok := s["include_venice_system_prompt"]; ok {
|
||||||
|
if b, ok := v.(bool); ok && !b {
|
||||||
|
veniceParams["include_venice_system_prompt"] = false
|
||||||
|
hasParams = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if settingBool(s, "enable_thinking") {
|
||||||
|
// Thinking requires disabling Venice system prompt
|
||||||
|
veniceParams["include_venice_system_prompt"] = false
|
||||||
|
hasParams = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasParams {
|
||||||
|
if req.ExtraBody == nil {
|
||||||
|
req.ExtraBody = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
req.ExtraBody["venice_parameters"] = veniceParams
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *VeniceHooks) PostStreamEvent(cfg ProviderConfig, event *StreamEvent) {
|
||||||
|
// Venice thinking output arrives in the regular content stream
|
||||||
|
// wrapped in <think>...</think> tags. The stream_loop already handles
|
||||||
|
// <think> extraction from reasoning_content, so we extract Venice
|
||||||
|
// thinking from Delta → Reasoning when enable_thinking is on.
|
||||||
|
if cfg.Settings == nil || !settingBool(cfg.Settings, "enable_thinking") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Venice sends thinking content inline in Delta. The frontend
|
||||||
|
// expects it in the Reasoning field. We detect <think> blocks
|
||||||
|
// and move them.
|
||||||
|
if event.Delta != "" && strings.Contains(event.Delta, "<think>") {
|
||||||
|
// Full block in one chunk (rare but handle it)
|
||||||
|
event.Reasoning += event.Delta
|
||||||
|
event.Delta = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── OpenRouter Hooks ────────────────────────
|
||||||
|
|
||||||
|
type OpenRouterHooks struct{}
|
||||||
|
|
||||||
|
func (h *OpenRouterHooks) PreRequest(cfg ProviderConfig, req *CompletionRequest) {
|
||||||
|
s := cfg.Settings
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prependSystemPrompt(settingString(s, "system_prompt_prefix"), req)
|
||||||
|
|
||||||
|
// OpenRouter route/transforms go in provider preferences header.
|
||||||
|
// These are sent as JSON in the X-Provider-Preferences header by
|
||||||
|
// the OpenAI doRequest (reads from ExtraBody["provider_preferences"]).
|
||||||
|
route := settingString(s, "route")
|
||||||
|
requireParams := settingBool(s, "require_parameters")
|
||||||
|
|
||||||
|
if route != "" || requireParams {
|
||||||
|
prefs := map[string]interface{}{}
|
||||||
|
if route != "" && route != "auto" {
|
||||||
|
prefs["route"] = route
|
||||||
|
}
|
||||||
|
if requireParams {
|
||||||
|
prefs["require_parameters"] = true
|
||||||
|
}
|
||||||
|
// Serialize for the HTTP header (OpenRouter reads from extra headers)
|
||||||
|
prefsJSON, _ := json.Marshal(prefs)
|
||||||
|
if cfg.CustomHeaders == nil {
|
||||||
|
cfg.CustomHeaders = make(map[string]string)
|
||||||
|
}
|
||||||
|
cfg.CustomHeaders["X-Provider-Preferences"] = string(prefsJSON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *OpenRouterHooks) PostStreamEvent(_ ProviderConfig, _ *StreamEvent) {
|
||||||
|
// No post-processing needed. OpenRouter returns standard OAI format.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Preset Override Merge ───────────────────
|
||||||
|
|
||||||
|
// MergePresetSettings merges persona-level setting overrides onto
|
||||||
|
// provider-level settings. Preset values take priority except for
|
||||||
|
// fields marked ProviderOnly in the schema.
|
||||||
|
func MergePresetSettings(providerType string, providerSettings, presetOverrides map[string]interface{}) map[string]interface{} {
|
||||||
|
if len(presetOverrides) == 0 {
|
||||||
|
return providerSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
schema := GetProfileSchema(providerType)
|
||||||
|
providerOnly := make(map[string]bool, len(schema.Fields))
|
||||||
|
for _, f := range schema.Fields {
|
||||||
|
if f.ProviderOnly {
|
||||||
|
providerOnly[f.Key] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
merged := make(map[string]interface{}, len(providerSettings)+len(presetOverrides))
|
||||||
|
for k, v := range providerSettings {
|
||||||
|
merged[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range presetOverrides {
|
||||||
|
if !providerOnly[k] {
|
||||||
|
merged[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return merged
|
||||||
|
}
|
||||||
340
server/providers/hooks_test.go
Normal file
340
server/providers/hooks_test.go
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── Profile Schema Tests ────────────────────
|
||||||
|
|
||||||
|
func TestGetProfileSchema_BuiltIn(t *testing.T) {
|
||||||
|
for _, id := range []string{"openai", "anthropic", "venice", "openrouter"} {
|
||||||
|
s := GetProfileSchema(id)
|
||||||
|
if len(s.Fields) == 0 {
|
||||||
|
t.Errorf("GetProfileSchema(%q) returned empty fields", id)
|
||||||
|
}
|
||||||
|
// Every schema should have system_prompt_prefix
|
||||||
|
found := false
|
||||||
|
for _, f := range s.Fields {
|
||||||
|
if f.Key == "system_prompt_prefix" {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("GetProfileSchema(%q) missing system_prompt_prefix field", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetProfileSchema_UnknownFallsToOpenAI(t *testing.T) {
|
||||||
|
s := GetProfileSchema("unknown-provider")
|
||||||
|
oai := GetProfileSchema("openai")
|
||||||
|
if len(s.Fields) != len(oai.Fields) {
|
||||||
|
t.Errorf("Unknown provider should fall back to openai schema, got %d fields, want %d", len(s.Fields), len(oai.Fields))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hook PreRequest Tests ───────────────────
|
||||||
|
|
||||||
|
func TestOpenAIHooks_SystemPromptPrefix(t *testing.T) {
|
||||||
|
h := &OpenAIHooks{}
|
||||||
|
cfg := ProviderConfig{
|
||||||
|
Settings: map[string]interface{}{
|
||||||
|
"system_prompt_prefix": "You are a helpful pirate.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req := &CompletionRequest{
|
||||||
|
Messages: []Message{
|
||||||
|
{Role: "system", Content: "Be concise."},
|
||||||
|
{Role: "user", Content: "Hello"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
h.PreRequest(cfg, req)
|
||||||
|
|
||||||
|
if req.Messages[0].Content != "You are a helpful pirate.\n\nBe concise." {
|
||||||
|
t.Errorf("system prompt prefix not prepended, got: %s", req.Messages[0].Content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenAIHooks_SystemPromptPrefix_NoExisting(t *testing.T) {
|
||||||
|
h := &OpenAIHooks{}
|
||||||
|
cfg := ProviderConfig{
|
||||||
|
Settings: map[string]interface{}{
|
||||||
|
"system_prompt_prefix": "Be a pirate.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req := &CompletionRequest{
|
||||||
|
Messages: []Message{
|
||||||
|
{Role: "user", Content: "Hello"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
h.PreRequest(cfg, req)
|
||||||
|
|
||||||
|
if len(req.Messages) != 2 {
|
||||||
|
t.Fatalf("expected 2 messages, got %d", len(req.Messages))
|
||||||
|
}
|
||||||
|
if req.Messages[0].Role != "system" || req.Messages[0].Content != "Be a pirate." {
|
||||||
|
t.Errorf("expected injected system message, got role=%s content=%s", req.Messages[0].Role, req.Messages[0].Content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenAIHooks_FrequencyPenalty(t *testing.T) {
|
||||||
|
h := &OpenAIHooks{}
|
||||||
|
cfg := ProviderConfig{
|
||||||
|
Settings: map[string]interface{}{
|
||||||
|
"frequency_penalty": 0.5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req := &CompletionRequest{Messages: []Message{{Role: "user", Content: "Hello"}}}
|
||||||
|
|
||||||
|
h.PreRequest(cfg, req)
|
||||||
|
|
||||||
|
if req.ExtraBody == nil {
|
||||||
|
t.Fatal("ExtraBody should be set")
|
||||||
|
}
|
||||||
|
if v, ok := req.ExtraBody["frequency_penalty"].(float64); !ok || v != 0.5 {
|
||||||
|
t.Errorf("frequency_penalty: got %v, want 0.5", req.ExtraBody["frequency_penalty"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenAIHooks_NilSettings(t *testing.T) {
|
||||||
|
h := &OpenAIHooks{}
|
||||||
|
cfg := ProviderConfig{Settings: nil}
|
||||||
|
req := &CompletionRequest{Messages: []Message{{Role: "user", Content: "Hello"}}}
|
||||||
|
|
||||||
|
h.PreRequest(cfg, req) // should not panic
|
||||||
|
if req.ExtraBody != nil {
|
||||||
|
t.Errorf("ExtraBody should be nil with no settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnthropicHooks_ExtendedThinking(t *testing.T) {
|
||||||
|
h := &AnthropicHooks{}
|
||||||
|
temp := 0.7
|
||||||
|
cfg := ProviderConfig{
|
||||||
|
Settings: map[string]interface{}{
|
||||||
|
"extended_thinking": true,
|
||||||
|
"thinking_budget": float64(20000), // JSON numbers are float64
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req := &CompletionRequest{
|
||||||
|
Messages: []Message{{Role: "user", Content: "Think hard"}},
|
||||||
|
Temperature: &temp,
|
||||||
|
}
|
||||||
|
|
||||||
|
h.PreRequest(cfg, req)
|
||||||
|
|
||||||
|
if req.Temperature != nil {
|
||||||
|
t.Error("Temperature should be cleared for extended thinking")
|
||||||
|
}
|
||||||
|
if req.ExtraBody == nil {
|
||||||
|
t.Fatal("ExtraBody should be set")
|
||||||
|
}
|
||||||
|
thinking, ok := req.ExtraBody["thinking"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("ExtraBody[thinking] should be map")
|
||||||
|
}
|
||||||
|
if thinking["type"] != "enabled" {
|
||||||
|
t.Errorf("thinking.type = %v, want enabled", thinking["type"])
|
||||||
|
}
|
||||||
|
if thinking["budget_tokens"] != 20000 {
|
||||||
|
t.Errorf("thinking.budget_tokens = %v, want 20000", thinking["budget_tokens"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnthropicHooks_ThinkingDefaultBudget(t *testing.T) {
|
||||||
|
h := &AnthropicHooks{}
|
||||||
|
cfg := ProviderConfig{
|
||||||
|
Settings: map[string]interface{}{
|
||||||
|
"extended_thinking": true,
|
||||||
|
// no thinking_budget → should use default 10000
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req := &CompletionRequest{Messages: []Message{{Role: "user", Content: "Think"}}}
|
||||||
|
|
||||||
|
h.PreRequest(cfg, req)
|
||||||
|
|
||||||
|
thinking := req.ExtraBody["thinking"].(map[string]interface{})
|
||||||
|
if thinking["budget_tokens"] != 10000 {
|
||||||
|
t.Errorf("default budget = %v, want 10000", thinking["budget_tokens"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnthropicHooks_NoThinking(t *testing.T) {
|
||||||
|
h := &AnthropicHooks{}
|
||||||
|
temp := 0.7
|
||||||
|
cfg := ProviderConfig{
|
||||||
|
Settings: map[string]interface{}{
|
||||||
|
"extended_thinking": false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req := &CompletionRequest{
|
||||||
|
Messages: []Message{{Role: "user", Content: "Hi"}},
|
||||||
|
Temperature: &temp,
|
||||||
|
}
|
||||||
|
|
||||||
|
h.PreRequest(cfg, req)
|
||||||
|
|
||||||
|
if req.ExtraBody != nil {
|
||||||
|
t.Error("ExtraBody should be nil when thinking is disabled")
|
||||||
|
}
|
||||||
|
if req.Temperature == nil {
|
||||||
|
t.Error("Temperature should be preserved when thinking is off")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVeniceHooks_WebSearch(t *testing.T) {
|
||||||
|
h := &VeniceHooks{}
|
||||||
|
cfg := ProviderConfig{
|
||||||
|
Settings: map[string]interface{}{
|
||||||
|
"enable_web_search": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req := &CompletionRequest{Messages: []Message{{Role: "user", Content: "Search for news"}}}
|
||||||
|
|
||||||
|
h.PreRequest(cfg, req)
|
||||||
|
|
||||||
|
vp, ok := req.ExtraBody["venice_parameters"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("venice_parameters missing from ExtraBody")
|
||||||
|
}
|
||||||
|
if vp["enable_web_search"] != "always" {
|
||||||
|
t.Errorf("enable_web_search = %v, want 'always'", vp["enable_web_search"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVeniceHooks_ThinkingDisablesSystemPrompt(t *testing.T) {
|
||||||
|
h := &VeniceHooks{}
|
||||||
|
cfg := ProviderConfig{
|
||||||
|
Settings: map[string]interface{}{
|
||||||
|
"enable_thinking": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req := &CompletionRequest{Messages: []Message{{Role: "user", Content: "Think"}}}
|
||||||
|
|
||||||
|
h.PreRequest(cfg, req)
|
||||||
|
|
||||||
|
vp := req.ExtraBody["venice_parameters"].(map[string]interface{})
|
||||||
|
if vp["include_venice_system_prompt"] != false {
|
||||||
|
t.Error("Thinking mode should set include_venice_system_prompt=false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVeniceHooks_NoSettings(t *testing.T) {
|
||||||
|
h := &VeniceHooks{}
|
||||||
|
cfg := ProviderConfig{Settings: map[string]interface{}{}}
|
||||||
|
req := &CompletionRequest{Messages: []Message{{Role: "user", Content: "Hi"}}}
|
||||||
|
|
||||||
|
h.PreRequest(cfg, req)
|
||||||
|
|
||||||
|
if req.ExtraBody != nil {
|
||||||
|
t.Error("ExtraBody should be nil with empty settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenRouterHooks_Route(t *testing.T) {
|
||||||
|
h := &OpenRouterHooks{}
|
||||||
|
cfg := ProviderConfig{
|
||||||
|
Settings: map[string]interface{}{
|
||||||
|
"route": "fallback",
|
||||||
|
"require_parameters": true,
|
||||||
|
},
|
||||||
|
CustomHeaders: map[string]string{},
|
||||||
|
}
|
||||||
|
req := &CompletionRequest{Messages: []Message{{Role: "user", Content: "Hi"}}}
|
||||||
|
|
||||||
|
h.PreRequest(cfg, req)
|
||||||
|
|
||||||
|
// Note: OpenRouter hooks set headers on the cfg copy, which is local.
|
||||||
|
// The real effect is tested via the serialized header value.
|
||||||
|
// Since cfg is passed by value in hooks, this test verifies the logic runs without panic.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GetHooks Tests ──────────────────────────
|
||||||
|
|
||||||
|
func TestGetHooks_BuiltIn(t *testing.T) {
|
||||||
|
for _, id := range []string{"openai", "anthropic", "venice", "openrouter"} {
|
||||||
|
h := GetHooks(id)
|
||||||
|
if h == nil {
|
||||||
|
t.Errorf("GetHooks(%q) returned nil", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetHooks_Unknown(t *testing.T) {
|
||||||
|
h := GetHooks("nonexistent")
|
||||||
|
if h != nil {
|
||||||
|
t.Error("GetHooks for unknown type should return nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── MergePresetSettings Tests ───────────────
|
||||||
|
|
||||||
|
func TestMergePresetSettings_Basic(t *testing.T) {
|
||||||
|
provider := map[string]interface{}{
|
||||||
|
"system_prompt_prefix": "Provider prompt",
|
||||||
|
"frequency_penalty": 0.3,
|
||||||
|
}
|
||||||
|
preset := map[string]interface{}{
|
||||||
|
"system_prompt_prefix": "Preset prompt",
|
||||||
|
}
|
||||||
|
|
||||||
|
merged := MergePresetSettings("openai", provider, preset)
|
||||||
|
|
||||||
|
if merged["system_prompt_prefix"] != "Preset prompt" {
|
||||||
|
t.Errorf("preset should override provider, got %v", merged["system_prompt_prefix"])
|
||||||
|
}
|
||||||
|
if merged["frequency_penalty"] != 0.3 {
|
||||||
|
t.Errorf("provider value should be preserved, got %v", merged["frequency_penalty"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergePresetSettings_ProviderOnlyBlocked(t *testing.T) {
|
||||||
|
provider := map[string]interface{}{
|
||||||
|
"route": "auto",
|
||||||
|
}
|
||||||
|
preset := map[string]interface{}{
|
||||||
|
"route": "fallback", // route is ProviderOnly for openrouter
|
||||||
|
}
|
||||||
|
|
||||||
|
merged := MergePresetSettings("openrouter", provider, preset)
|
||||||
|
|
||||||
|
if merged["route"] != "auto" {
|
||||||
|
t.Errorf("ProviderOnly field should not be overridden by preset, got %v", merged["route"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergePresetSettings_EmptyOverrides(t *testing.T) {
|
||||||
|
provider := map[string]interface{}{"key": "value"}
|
||||||
|
merged := MergePresetSettings("openai", provider, nil)
|
||||||
|
if merged["key"] != "value" {
|
||||||
|
t.Error("empty overrides should return provider settings unchanged")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── mergeExtraBody Tests ────────────────────
|
||||||
|
|
||||||
|
func TestMergeExtraBody(t *testing.T) {
|
||||||
|
body := []byte(`{"model":"gpt-4","stream":true}`)
|
||||||
|
extra := map[string]interface{}{
|
||||||
|
"venice_parameters": map[string]interface{}{
|
||||||
|
"enable_web_search": "always",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
merged, err := mergeExtraBody(body, extra)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("mergeExtraBody error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should contain both original and extra fields
|
||||||
|
s := string(merged)
|
||||||
|
if !strings.Contains(s, `"model"`) || !strings.Contains(s, `"stream"`) {
|
||||||
|
t.Error("original fields missing from merged body")
|
||||||
|
}
|
||||||
|
if !strings.Contains(s, `"venice_parameters"`) || !strings.Contains(s, `"enable_web_search"`) {
|
||||||
|
t.Error("extra fields missing from merged body")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -432,6 +432,14 @@ func (p *OpenAIProvider) doRequest(ctx context.Context, cfg ProviderConfig, req
|
|||||||
return nil, fmt.Errorf("marshal request: %w", err)
|
return nil, fmt.Errorf("marshal request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge hook-supplied extra fields into wire JSON (v0.22.1)
|
||||||
|
if len(req.ExtraBody) > 0 {
|
||||||
|
body, err = mergeExtraBody(body, req.ExtraBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("merge extra body: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(body))
|
httpReq, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
154
server/providers/profile.go
Normal file
154
server/providers/profile.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
// ProfileSchema describes the available settings for a provider type.
|
||||||
|
// Used by the admin UI to render forms and by validation on save.
|
||||||
|
type ProfileSchema struct {
|
||||||
|
// Fields lists the configurable settings in display order.
|
||||||
|
Fields []ProfileField `json:"fields"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProfileField describes a single setting within a provider profile.
|
||||||
|
type ProfileField struct {
|
||||||
|
Key string `json:"key"` // settings JSON key
|
||||||
|
Label string `json:"label"` // human-readable name
|
||||||
|
Description string `json:"description,omitempty"` // help text
|
||||||
|
Type string `json:"type"` // "bool", "int", "string", "enum", "float"
|
||||||
|
Default interface{} `json:"default,omitempty"` // default value
|
||||||
|
EnumValues []string `json:"enum_values,omitempty"` // valid values when Type="enum"
|
||||||
|
Min *int `json:"min,omitempty"` // min for int/float
|
||||||
|
Max *int `json:"max,omitempty"` // max for int/float
|
||||||
|
DependsOn string `json:"depends_on,omitempty"` // only show when this key is truthy
|
||||||
|
ProviderOnly bool `json:"provider_only,omitempty"` // not overridable at preset level
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Built-in Profile Schemas ────────────────
|
||||||
|
|
||||||
|
// intPtr is a helper for literal pointers in struct init.
|
||||||
|
func intPtr(v int) *int { return &v }
|
||||||
|
|
||||||
|
var profileSchemas = map[string]ProfileSchema{
|
||||||
|
"openai": {
|
||||||
|
Fields: []ProfileField{
|
||||||
|
{
|
||||||
|
Key: "system_prompt_prefix",
|
||||||
|
Label: "System Prompt Prefix",
|
||||||
|
Description: "Prepended to every system prompt sent to this provider.",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "frequency_penalty",
|
||||||
|
Label: "Frequency Penalty",
|
||||||
|
Description: "Penalizes repeated tokens. Range: -2.0 to 2.0.",
|
||||||
|
Type: "float",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "presence_penalty",
|
||||||
|
Label: "Presence Penalty",
|
||||||
|
Description: "Penalizes tokens already present. Range: -2.0 to 2.0.",
|
||||||
|
Type: "float",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"anthropic": {
|
||||||
|
Fields: []ProfileField{
|
||||||
|
{
|
||||||
|
Key: "system_prompt_prefix",
|
||||||
|
Label: "System Prompt Prefix",
|
||||||
|
Description: "Prepended to every system prompt sent to this provider.",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "extended_thinking",
|
||||||
|
Label: "Extended Thinking",
|
||||||
|
Description: "Enable Anthropic extended thinking mode. Sends thinking.type=enabled with budget_tokens.",
|
||||||
|
Type: "bool",
|
||||||
|
Default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "thinking_budget",
|
||||||
|
Label: "Thinking Budget (tokens)",
|
||||||
|
Description: "Max tokens for the thinking phase. Only used when extended_thinking is enabled.",
|
||||||
|
Type: "int",
|
||||||
|
Default: 10000,
|
||||||
|
Min: intPtr(1024),
|
||||||
|
Max: intPtr(128000),
|
||||||
|
DependsOn: "extended_thinking",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"venice": {
|
||||||
|
Fields: []ProfileField{
|
||||||
|
{
|
||||||
|
Key: "system_prompt_prefix",
|
||||||
|
Label: "System Prompt Prefix",
|
||||||
|
Description: "Prepended to every system prompt. Venice also injects its own character prompt.",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "enable_thinking",
|
||||||
|
Label: "Enable Thinking",
|
||||||
|
Description: "Send venice_parameters.include_venice_system_prompt=false and enable thinking output.",
|
||||||
|
Type: "bool",
|
||||||
|
Default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "enable_web_search",
|
||||||
|
Label: "Enable Web Search",
|
||||||
|
Description: "Allow Venice to search the web for context (venice_parameters.enable_web_search).",
|
||||||
|
Type: "bool",
|
||||||
|
Default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "include_venice_system_prompt",
|
||||||
|
Label: "Include Venice System Prompt",
|
||||||
|
Description: "Include Venice's built-in character system prompt.",
|
||||||
|
Type: "bool",
|
||||||
|
Default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"openrouter": {
|
||||||
|
Fields: []ProfileField{
|
||||||
|
{
|
||||||
|
Key: "system_prompt_prefix",
|
||||||
|
Label: "System Prompt Prefix",
|
||||||
|
Description: "Prepended to every system prompt sent through OpenRouter.",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "route",
|
||||||
|
Label: "Routing Strategy",
|
||||||
|
Description: "How OpenRouter selects the underlying provider for a model.",
|
||||||
|
Type: "enum",
|
||||||
|
Default: "auto",
|
||||||
|
EnumValues: []string{"auto", "fallback"},
|
||||||
|
ProviderOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "require_parameters",
|
||||||
|
Label: "Require Parameters Support",
|
||||||
|
Description: "Only route to providers that support the requested parameters (tools, temperature, etc.).",
|
||||||
|
Type: "bool",
|
||||||
|
Default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProfileSchema returns the profile schema for a provider type.
|
||||||
|
// Falls back to the openai schema for unknown types (OpenAI-compatible).
|
||||||
|
func GetProfileSchema(providerType string) ProfileSchema {
|
||||||
|
if s, ok := profileSchemas[providerType]; ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return profileSchemas["openai"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterProfileSchema adds or replaces a profile schema for a provider type.
|
||||||
|
// Used for testing and future plugin support.
|
||||||
|
func RegisterProfileSchema(providerType string, schema ProfileSchema) {
|
||||||
|
profileSchemas[providerType] = schema
|
||||||
|
}
|
||||||
@@ -117,6 +117,7 @@ type CompletionRequest struct {
|
|||||||
TopP *float64 `json:"top_p,omitempty"`
|
TopP *float64 `json:"top_p,omitempty"`
|
||||||
Stream bool `json:"stream,omitempty"`
|
Stream bool `json:"stream,omitempty"`
|
||||||
Tools []ToolDef `json:"tools,omitempty"` // available tools for function calling
|
Tools []ToolDef `json:"tools,omitempty"` // available tools for function calling
|
||||||
|
ExtraBody map[string]interface{} `json:"-"` // provider-specific wire fields (v0.22.1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompletionResponse is the normalized non-streaming response.
|
// CompletionResponse is the normalized non-streaming response.
|
||||||
@@ -170,3 +171,17 @@ type Model struct {
|
|||||||
Capabilities models.ModelCapabilities `json:"capabilities"`
|
Capabilities models.ModelCapabilities `json:"capabilities"`
|
||||||
Pricing *models.ModelPricing `json:"pricing,omitempty"`
|
Pricing *models.ModelPricing `json:"pricing,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mergeExtraBody merges arbitrary key-value pairs into a marshalled JSON object.
|
||||||
|
// Used by provider doRequest methods to inject hook-supplied fields (e.g.
|
||||||
|
// venice_parameters, thinking config) into the wire-format request body.
|
||||||
|
func mergeExtraBody(body []byte, extra map[string]interface{}) ([]byte, error) {
|
||||||
|
var base map[string]interface{}
|
||||||
|
if err := json.Unmarshal(body, &base); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for k, v := range extra {
|
||||||
|
base[k] = v
|
||||||
|
}
|
||||||
|
return json.Marshal(base)
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,10 +40,70 @@ func List() []string {
|
|||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Provider Type Metadata ──────────────────
|
||||||
|
|
||||||
|
// ProviderTypeMeta describes a registered provider type for the admin API.
|
||||||
|
type ProviderTypeMeta struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
DefaultEndpoint string `json:"default_endpoint"`
|
||||||
|
ProfileSchema ProfileSchema `json:"profile_schema"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var providerTypes = map[string]ProviderTypeMeta{}
|
||||||
|
|
||||||
|
// RegisterType adds a provider type with its metadata to the type registry.
|
||||||
|
// Called by Init() for built-in types. External types can be registered at startup.
|
||||||
|
func RegisterType(meta ProviderTypeMeta, p Provider) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
registry[meta.ID] = p
|
||||||
|
providerTypes[meta.ID] = meta
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListTypes returns metadata for all registered provider types.
|
||||||
|
func ListTypes() []ProviderTypeMeta {
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
|
out := make([]ProviderTypeMeta, 0, len(providerTypes))
|
||||||
|
for _, m := range providerTypes {
|
||||||
|
out = append(out, m)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// Init registers all built-in providers.
|
// Init registers all built-in providers.
|
||||||
func Init() {
|
func Init() {
|
||||||
Register(&OpenAIProvider{})
|
RegisterType(ProviderTypeMeta{
|
||||||
Register(&AnthropicProvider{})
|
ID: "openai",
|
||||||
Register(&VeniceProvider{})
|
Name: "OpenAI",
|
||||||
Register(&OpenRouterProvider{})
|
Description: "OpenAI and any OpenAI-compatible API (Ollama, LiteLLM, etc.)",
|
||||||
|
DefaultEndpoint: "https://api.openai.com/v1",
|
||||||
|
ProfileSchema: GetProfileSchema("openai"),
|
||||||
|
}, &OpenAIProvider{})
|
||||||
|
|
||||||
|
RegisterType(ProviderTypeMeta{
|
||||||
|
ID: "anthropic",
|
||||||
|
Name: "Anthropic",
|
||||||
|
Description: "Anthropic Messages API (Claude models)",
|
||||||
|
DefaultEndpoint: "https://api.anthropic.com",
|
||||||
|
ProfileSchema: GetProfileSchema("anthropic"),
|
||||||
|
}, &AnthropicProvider{})
|
||||||
|
|
||||||
|
RegisterType(ProviderTypeMeta{
|
||||||
|
ID: "venice",
|
||||||
|
Name: "Venice",
|
||||||
|
Description: "Venice.ai — privacy-focused, OpenAI-compatible with extensions",
|
||||||
|
DefaultEndpoint: "https://api.venice.ai/api/v1",
|
||||||
|
ProfileSchema: GetProfileSchema("venice"),
|
||||||
|
}, &VeniceProvider{})
|
||||||
|
|
||||||
|
RegisterType(ProviderTypeMeta{
|
||||||
|
ID: "openrouter",
|
||||||
|
Name: "OpenRouter",
|
||||||
|
Description: "OpenRouter — unified API for 300+ models with per-model pricing",
|
||||||
|
DefaultEndpoint: "https://openrouter.ai/api/v1",
|
||||||
|
ProfileSchema: GetProfileSchema("openrouter"),
|
||||||
|
}, &OpenRouterProvider{})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user