use aliases for claude and gemini providers

This commit is contained in:
david 2026-08-24 18:16:31 +03:00
parent a17c91b4be
commit 41975ebc71
12 changed files with 74 additions and 92 deletions

View File

@ -65,7 +65,7 @@ by picking a provider in `docker/env/core.local.env`; the model is
optional and defaults to the provider's strongest supported one:
```sh
OXYNOTE_CORE_ASSISTANT_PROVIDER=claude
OXYNOTE_CORE_ASSISTANT_PROVIDER=anthropic
OXYNOTE_CORE_ASSISTANT_MODEL=claude-opus-4-6
OXYNOTE_CORE_ASSISTANT_API_KEY=sk-...
```
@ -74,9 +74,9 @@ Switching providers is an env change and a restart — no rebuild.
| provider | credentials | supported models |
| --- | --- | --- |
| `claude` | `ASSISTANT_API_KEY`, or `ASSISTANT_BEDROCK_*` / `ASSISTANT_VERTEX_*` | `claude-fable-5`, `claude-opus-5` (default), `claude-opus-4-6`, `claude-sonnet-5`, `claude-sonnet-4-6`, `claude-haiku-4-5` |
| `openai` | `ASSISTANT_API_KEY` (+ `ASSISTANT_AZURE_API_VERSION` for Azure) | `gpt-5.1` (default), `gpt-5`, `gpt-5-mini`, `gpt-5-nano` |
| `gemini` | `ASSISTANT_API_KEY` | `gemini-3-pro-preview`, `gemini-2.5-pro` (default), `gemini-2.5-flash`, `gemini-2.5-flash-lite` |
| `anthropic` | `ASSISTANT_API_KEY`, or `ASSISTANT_BEDROCK_*` / `ASSISTANT_VERTEX_*` | `claude-fable-5`, `claude-opus-5` (default), `claude-opus-4-6`, `claude-sonnet-5`, `claude-sonnet-4-6` |
| `openai` | `ASSISTANT_API_KEY` (+ `ASSISTANT_AZURE_API_VERSION` for Azure) | `gpt-5.1` (default), `gpt-5`, `gpt-5-mini` |
| `google` | `ASSISTANT_API_KEY` | `gemini-3-pro-preview`, `gemini-2.5-pro` (default), `gemini-2.5-flash` |
| `ollama` | none; set `ASSISTANT_BASE_URL` to your server | `llama3.3:70b` (default), `qwen3:32b` |
| `openrouter` | `ASSISTANT_API_KEY` | the models above behind their vendor prefix (`anthropic/claude-opus-5` (default), `openai/gpt-5`, `google/gemini-2.5-pro`, …) |

View File

@ -60,7 +60,7 @@ OXYNOTE_CORE_SLACK_REDIRECT_URL=http://localhost:8080/slack
OXYNOTE_CORE_SLACK_INSTALLATION_SIGNING_SECRET=
# AI assistant. An empty PROVIDER disables the assistant: core boots,
# the chat endpoint refuses, and the MCP surface keeps working. To
# enable it, set PROVIDER to the vendor (claude, openai, gemini, ollama,
# enable it, set PROVIDER to the vendor (anthropic, openai, google, ollama,
# or openrouter) and the credentials the vendor needs (an API key for
# every provider except ollama); a set provider with missing pieces is a
# boot error. MODEL is the vendor's own model identifier and is
@ -85,13 +85,13 @@ OXYNOTE_CORE_ASSISTANT_REQUEST_TIMEOUT=10m
OXYNOTE_CORE_ASSISTANT_SUMMARY_MODEL=
# optional, PROVIDER=openai only: set to target Azure OpenAI.
OXYNOTE_CORE_ASSISTANT_AZURE_API_VERSION=
# optional, PROVIDER=claude only: set REGION to reach Claude via AWS
# optional, PROVIDER=anthropic only: set REGION to reach Claude via AWS
# Bedrock. Leave the keys empty to use the ambient AWS credential chain.
OXYNOTE_CORE_ASSISTANT_BEDROCK_REGION=
OXYNOTE_CORE_ASSISTANT_BEDROCK_ACCESS_KEY=
OXYNOTE_CORE_ASSISTANT_BEDROCK_SECRET_ACCESS_KEY=
OXYNOTE_CORE_ASSISTANT_BEDROCK_SESSION_TOKEN=
# optional, PROVIDER=claude only: set PROJECT_ID to reach Claude via
# optional, PROVIDER=anthropic only: set PROJECT_ID to reach Claude via
# Google Vertex. Leave the service account empty to use application
# default credentials.
OXYNOTE_CORE_ASSISTANT_VERTEX_PROJECT_ID=

View File

@ -544,7 +544,7 @@ func assistantProviderOptions() (provider.Options, error) {
Enabled: buildinfo.Getenv("ASSISTANT_AZURE_API_VERSION") != "",
APIVersion: buildinfo.Getenv("ASSISTANT_AZURE_API_VERSION"),
}
case provider.ProviderClaude:
case provider.ProviderAnthropic:
opts.Bedrock = provider.BedrockOptions{
Enabled: buildinfo.Getenv("ASSISTANT_BEDROCK_REGION") != "",
Region: buildinfo.Getenv("ASSISTANT_BEDROCK_REGION"),
@ -558,7 +558,7 @@ func assistantProviderOptions() (provider.Options, error) {
Region: buildinfo.Getenv("ASSISTANT_VERTEX_REGION"),
ServiceAccountJSON: []byte(buildinfo.Getenv("ASSISTANT_VERTEX_SERVICE_ACCOUNT_JSON")),
}
case provider.ProviderGemini, provider.ProviderOllama, provider.ProviderOpenRouter:
case provider.ProviderGoogle, provider.ProviderOllama, provider.ProviderOpenRouter:
// no vendor-specific settings beyond the common ones.
}
@ -597,13 +597,13 @@ func assistantModels(ctx context.Context, log *slog.Logger) (
}
switch status {
case provider.StatusActive:
case provider.StatusActive, provider.StatusInactive:
// nothing to warn about: full strength needs no caveat, and
// inactive only arrives alongside an error handled above.
case provider.StatusActiveButWeak:
log.Warn("assistant model is weaker than recommended", slog.String("model", opts.Model))
case provider.StatusInactiveTooWeak:
log.Warn("assistant model is too weak to run the assistant", slog.String("model", opts.Model))
case provider.StatusInactive:
log.Warn("assistant model is not supported", slog.String("model", opts.Model))
}
if !status.Active() {

View File

@ -19,14 +19,14 @@ func Test_newClaude(t *testing.T) {
}{
"Direct with api key": {
Opts: Options{
Provider: ProviderClaude,
Provider: ProviderAnthropic,
Model: "claude-opus-4-6",
APIKey: "k",
},
},
"Bedrock needs no api key": {
Opts: Options{
Provider: ProviderClaude,
Provider: ProviderAnthropic,
Model: "anthropic.claude-opus-4-6",
Bedrock: BedrockOptions{
Enabled: true,
@ -37,11 +37,11 @@ func Test_newClaude(t *testing.T) {
},
},
"Optional tuning": {
Opts: tunedOptions(ProviderClaude, "claude-opus-4-6"),
Opts: tunedOptions(ProviderAnthropic, "claude-opus-4-6"),
},
"Vertex without a project id": {
Opts: Options{
Provider: ProviderClaude,
Provider: ProviderAnthropic,
Model: "claude-opus-4-6",
Vertex: VertexOptions{Enabled: true, Region: "us-east5"},
},

View File

@ -24,7 +24,7 @@ func Test_newGemini(t *testing.T) {
}{
"Defaults": {
Opts: Options{
Provider: ProviderGemini,
Provider: ProviderGoogle,
Model: "gemini-2.5-pro",
APIKey: "k",
},
@ -38,7 +38,7 @@ func Test_newGemini(t *testing.T) {
Config: &gemini.Config{Model: "gemini-2.5-pro"},
},
"Optional tuning": {
Opts: tunedOptions(ProviderGemini, "gemini-2.5-pro"),
Opts: tunedOptions(ProviderGoogle, "gemini-2.5-pro"),
Client: &genai.ClientConfig{
APIKey: "k",
Backend: genai.BackendGeminiAPI,

View File

@ -1,5 +1,5 @@
{
"claude": {
"anthropic": {
"default": "claude-opus-5",
"models": {
"active": [
@ -10,9 +10,6 @@
"active-but-weak": [
"claude-sonnet-5",
"claude-sonnet-4-6"
],
"inactive-too-weak": [
"claude-haiku-4-5"
]
}
},
@ -25,13 +22,10 @@
],
"active-but-weak": [
"gpt-5-mini"
],
"inactive-too-weak": [
"gpt-5-nano"
]
}
},
"gemini": {
"google": {
"default": "gemini-2.5-pro",
"models": {
"active": [
@ -40,9 +34,6 @@
],
"active-but-weak": [
"gemini-2.5-flash"
],
"inactive-too-weak": [
"gemini-2.5-flash-lite"
]
}
},
@ -72,11 +63,6 @@
"anthropic/claude-sonnet-4-6",
"openai/gpt-5-mini",
"google/gemini-2.5-flash"
],
"inactive-too-weak": [
"anthropic/claude-haiku-4-5",
"openai/gpt-5-nano",
"google/gemini-2.5-flash-lite"
]
}
}

View File

@ -64,11 +64,11 @@ type Options struct {
Azure AzureOptions
// Bedrock holds AWS Bedrock settings, read only when Provider is
// ProviderClaude.
// ProviderAnthropic.
Bedrock BedrockOptions
// Vertex holds Google Vertex settings, read only when Provider is
// ProviderClaude.
// ProviderAnthropic.
Vertex VertexOptions
}
@ -146,7 +146,7 @@ func (o Options) requiresAPIKey() bool {
return false
}
if o.Provider == ProviderClaude {
if o.Provider == ProviderAnthropic {
return !o.Bedrock.Enabled && !o.Vertex.Enabled
}

View File

@ -32,7 +32,7 @@ func Test_Options_validate(t *testing.T) {
Err: ErrMissingAPIKey,
},
"Gemini without api key": {
Opts: Options{Provider: ProviderGemini, Model: "gemini-2.5-pro"},
Opts: Options{Provider: ProviderGoogle, Model: "gemini-2.5-pro"},
Err: ErrMissingAPIKey,
},
"OpenRouter without api key": {
@ -40,22 +40,22 @@ func Test_Options_validate(t *testing.T) {
Err: ErrMissingAPIKey,
},
"Claude without api key or cloud credentials": {
Opts: Options{Provider: ProviderClaude, Model: "claude-opus-4-6"},
Opts: Options{Provider: ProviderAnthropic, Model: "claude-opus-4-6"},
Err: ErrMissingAPIKey,
},
"Claude with api key": {
Opts: Options{Provider: ProviderClaude, Model: "claude-opus-4-6", APIKey: "k"},
Opts: Options{Provider: ProviderAnthropic, Model: "claude-opus-4-6", APIKey: "k"},
},
"Claude via bedrock": {
Opts: Options{
Provider: ProviderClaude,
Provider: ProviderAnthropic,
Model: "claude-opus-4-6",
Bedrock: BedrockOptions{Enabled: true},
},
},
"Claude via vertex": {
Opts: Options{
Provider: ProviderClaude,
Provider: ProviderAnthropic,
Model: "claude-opus-4-6",
Vertex: VertexOptions{Enabled: true},
},
@ -89,11 +89,11 @@ func Test_Options_requiresAPIKey(t *testing.T) {
}{
"Ollama never does": {Opts: Options{Provider: ProviderOllama}},
"OpenAI does": {Opts: Options{Provider: ProviderOpenAI}, Result: true},
"Gemini does": {Opts: Options{Provider: ProviderGemini}, Result: true},
"Gemini does": {Opts: Options{Provider: ProviderGoogle}, Result: true},
"OpenRouter does": {Opts: Options{Provider: ProviderOpenRouter}, Result: true},
"Claude does when direct": {Opts: Options{Provider: ProviderClaude}, Result: true},
"Claude does not with bedrock": {Opts: Options{Provider: ProviderClaude, Bedrock: BedrockOptions{Enabled: true}}},
"Claude does not with vertex": {Opts: Options{Provider: ProviderClaude, Vertex: VertexOptions{Enabled: true}}},
"Claude does when direct": {Opts: Options{Provider: ProviderAnthropic}, Result: true},
"Claude does not with bedrock": {Opts: Options{Provider: ProviderAnthropic, Bedrock: BedrockOptions{Enabled: true}}},
"Claude does not with vertex": {Opts: Options{Provider: ProviderAnthropic, Vertex: VertexOptions{Enabled: true}}},
}
for cn, c := range cc {

View File

@ -15,16 +15,16 @@ import (
)
const (
// ProviderClaude specifies Anthropic's Claude, reachable directly or
// through AWS Bedrock and Google Vertex.
ProviderClaude Provider = "claude"
// ProviderAnthropic specifies Anthropic's Claude models, reachable
// directly or through AWS Bedrock and Google Vertex.
ProviderAnthropic Provider = "anthropic"
// ProviderOpenAI specifies OpenAI, Azure OpenAI, and any endpoint
// speaking the OpenAI chat-completions protocol.
ProviderOpenAI Provider = "openai"
// ProviderGemini specifies Google's Gemini models.
ProviderGemini Provider = "gemini"
// ProviderGoogle specifies Google's Gemini models.
ProviderGoogle Provider = "google"
// ProviderOllama specifies a self-hosted Ollama server.
ProviderOllama Provider = "ollama"
@ -44,9 +44,9 @@ var ErrInvalidProvider = errors.New("invalid assistant provider")
// _providers lists every supported provider in the order they are
// reported to the user, so the error message reads the same every time.
var _providers = []Provider{
ProviderClaude,
ProviderAnthropic,
ProviderOpenAI,
ProviderGemini,
ProviderGoogle,
ProviderOllama,
ProviderOpenRouter,
}
@ -89,11 +89,11 @@ func New(ctx context.Context, opts Options) (model.ToolCallingChatModel, error)
}
switch opts.Provider {
case ProviderClaude:
case ProviderAnthropic:
return newClaude(ctx, opts)
case ProviderOpenAI:
return newOpenAI(ctx, opts)
case ProviderGemini:
case ProviderGoogle:
return newGemini(ctx, opts)
case ProviderOllama:
return newOllama(ctx, opts)

View File

@ -25,9 +25,9 @@ func Test_Provider_Validate(t *testing.T) {
Provider Provider
Err error
}{
"Claude is supported": {Provider: ProviderClaude},
"Anthropic is supported": {Provider: ProviderAnthropic},
"OpenAI is supported": {Provider: ProviderOpenAI},
"Gemini is supported": {Provider: ProviderGemini},
"Google is supported": {Provider: ProviderGoogle},
"Ollama is supported": {Provider: ProviderOllama},
"OpenRouter is supported": {Provider: ProviderOpenRouter},
"Unknown provider": {
@ -68,9 +68,9 @@ func Test_ParseProvider(t *testing.T) {
Inp string
Result Provider
}{
"Exact match": {Inp: "claude", Result: ProviderClaude},
"Exact match": {Inp: "anthropic", Result: ProviderAnthropic},
"Mixed case": {Inp: "OpenAI", Result: ProviderOpenAI},
"Surrounding spaces": {Inp: " gemini ", Result: ProviderGemini},
"Surrounding spaces": {Inp: " google ", Result: ProviderGoogle},
"Upper case and tabs": {Inp: "\tOLLAMA\n", Result: ProviderOllama},
"Unknown passes through": {
Inp: "cohere",
@ -91,7 +91,7 @@ func Test_ParseProvider(t *testing.T) {
func Test_supported(t *testing.T) {
t.Parallel()
assert.Equal(t, "claude, openai, gemini, ollama, openrouter", supported())
assert.Equal(t, "anthropic, openai, google, ollama, openrouter", supported())
}
func Test_New(t *testing.T) {
@ -108,7 +108,7 @@ func Test_New(t *testing.T) {
ErrContains: "not supported",
},
"Missing model": {
Opts: Options{Provider: ProviderClaude, APIKey: "k"},
Opts: Options{Provider: ProviderAnthropic, APIKey: "k"},
Err: ErrMissingModel,
},
"Missing api key": {
@ -117,7 +117,7 @@ func Test_New(t *testing.T) {
},
"Claude": {
Opts: Options{
Provider: ProviderClaude,
Provider: ProviderAnthropic,
Model: "claude-opus-4-6",
APIKey: "k",
},
@ -131,7 +131,7 @@ func Test_New(t *testing.T) {
},
"Gemini": {
Opts: Options{
Provider: ProviderGemini,
Provider: ProviderGoogle,
Model: "gemini-2.5-pro",
APIKey: "k",
},

View File

@ -16,13 +16,14 @@ const (
// assistant, but with limitations the operator should expect.
StatusActiveButWeak Status = "active-but-weak"
// StatusInactiveTooWeak specifies that the configured model is too
// weak to run the assistant, which is therefore disabled.
// StatusInactiveTooWeak specifies that the configured model is not
// known to be strong enough to run the assistant — it is either
// listed as too weak or not listed at all — so the assistant is
// disabled.
StatusInactiveTooWeak Status = "inactive-too-weak"
// StatusInactive specifies that the assistant is disabled: either
// no provider is configured or the configured model is not one this
// build supports.
// StatusInactive specifies that the assistant is disabled because
// no provider is configured.
StatusInactive Status = "inactive"
)
@ -42,13 +43,14 @@ type modelList struct {
Default string `json:"default"`
// Models groups the supported model ids by the status running the
// assistant on them yields. A model in no group is not supported
// and reports StatusInactive.
// assistant on them yields. Only the two active tiers are listed:
// a model in no group is StatusInactiveTooWeak.
Models map[Status][]string `json:"models"`
}
// status returns the status running the assistant on the given model
// yields, or StatusInactive when the model is not on the list.
// yields. A model outside the list is not known to be strong enough,
// so it reports StatusInactiveTooWeak.
func (ml modelList) status(model string) Status {
for status, ids := range ml.Models {
if slices.Contains(ids, model) {
@ -56,7 +58,7 @@ func (ml modelList) status(model string) Status {
}
}
return StatusInactive
return StatusInactiveTooWeak
}
// _modelsJSON is the embedded per-provider model list: every supported
@ -75,7 +77,7 @@ var _models, _modelsErr = parseModels(_modelsJSON)
// ModelStatus validates the options and classifies the configured
// model against the provider's supported list: whether the assistant
// runs at full strength, runs with limitations, or does not run at
// all. A model outside the list reports StatusInactive.
// all. A model outside the list reports StatusInactiveTooWeak.
func (o Options) ModelStatus() (Status, error) {
if _modelsErr != nil {
// NOCOV: the embedded file is pinned valid by the parse test.

View File

@ -40,23 +40,19 @@ func Test_Options_ModelStatus(t *testing.T) {
Err error
}{
"Invalid options": {
Opts: Options{Provider: ProviderClaude, Model: "claude-opus-5"},
Opts: Options{Provider: ProviderAnthropic, Model: "claude-opus-5"},
Err: ErrMissingAPIKey,
},
"Unsupported model": {
Opts: Options{Provider: ProviderClaude, Model: "claude-2", APIKey: "k"},
Result: StatusInactive,
},
"Full strength model": {
Opts: Options{Provider: ProviderClaude, Model: "claude-opus-5", APIKey: "k"},
Opts: Options{Provider: ProviderAnthropic, Model: "claude-opus-5", APIKey: "k"},
Result: StatusActive,
},
"Weaker model": {
Opts: Options{Provider: ProviderClaude, Model: "claude-sonnet-5", APIKey: "k"},
Opts: Options{Provider: ProviderAnthropic, Model: "claude-sonnet-5", APIKey: "k"},
Result: StatusActiveButWeak,
},
"Too weak model": {
Opts: Options{Provider: ProviderClaude, Model: "claude-haiku-4-5", APIKey: "k"},
"Unlisted model": {
Opts: Options{Provider: ProviderAnthropic, Model: "claude-haiku-4-5", APIKey: "k"},
Result: StatusInactiveTooWeak,
},
"Supported ollama model": {
@ -96,9 +92,9 @@ func Test_Provider_DefaultModel(t *testing.T) {
Provider Provider
Result string
}{
"Claude": {Provider: ProviderClaude, Result: "claude-opus-5"},
"Claude": {Provider: ProviderAnthropic, Result: "claude-opus-5"},
"OpenAI": {Provider: ProviderOpenAI, Result: "gpt-5.1"},
"Gemini": {Provider: ProviderGemini, Result: "gemini-2.5-pro"},
"Gemini": {Provider: ProviderGoogle, Result: "gemini-2.5-pro"},
"Ollama": {Provider: ProviderOllama, Result: "llama3.3:70b"},
"OpenRouter": {Provider: ProviderOpenRouter, Result: "anthropic/claude-opus-5"},
"Unknown provider": {Provider: "cohere"},
@ -117,7 +113,7 @@ func Test_Provider_DefaultModel(t *testing.T) {
// a default that is not on its provider's supported list
// would disable the assistant instead of enabling it.
assert.NotEqual(t, StatusInactive, _models[c.Provider].status(result))
assert.True(t, _models[c.Provider].status(result).Active())
})
}
}
@ -145,14 +141,14 @@ func Test_parseModels(t *testing.T) {
for p, ml := range models {
require.NoError(t, p.Validate())
assert.NotEqual(t, StatusInactive, ml.status(ml.Default), p)
assert.True(t, ml.status(ml.Default).Active(), p)
seen := make(map[string]bool)
for status, ids := range ml.Models {
assert.Contains(
t,
[]Status{StatusActive, StatusActiveButWeak, StatusInactiveTooWeak},
[]Status{StatusActive, StatusActiveButWeak},
status,
string(p),
)
@ -171,9 +167,8 @@ func Test_modelList_status(t *testing.T) {
ml := modelList{
Models: map[Status][]string{
StatusActive: {"strong"},
StatusActiveButWeak: {"medium"},
StatusInactiveTooWeak: {"small"},
StatusActive: {"strong"},
StatusActiveButWeak: {"medium"},
},
}
@ -183,8 +178,7 @@ func Test_modelList_status(t *testing.T) {
}{
"Full strength model": {Model: "strong", Result: StatusActive},
"Weaker model": {Model: "medium", Result: StatusActiveButWeak},
"Too weak model": {Model: "small", Result: StatusInactiveTooWeak},
"Unlisted model": {Model: "unknown", Result: StatusInactive},
"Unlisted model": {Model: "unknown", Result: StatusInactiveTooWeak},
}
for cn, c := range cc {