mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 08:18:19 +00:00
feat: add capability-aware reasoning effort controls (#593)
* feat(reasoning): add capability-aware effort resolution - resolve requested reasoning levels against exact model capabilities - persist requested effort on agents and expose effective effort in traces - add backend tests for provider models, agent store, and resolution logic Refs #591 * feat(ui): gate reasoning controls by model capabilities - only show supported reasoning levels when provider model metadata is available - preserve expert reasoning selections during async model loading - surface effective reasoning details in trace dialogs and localized copy Refs #591 * docs(api): document capability-aware reasoning controls - describe exact-match capability lookup and downgrade behavior - update provider model metadata and trace response documentation - refresh the generated OpenAPI spec for the new reasoning fields Refs #591 * feat: add provider-first reasoning controls * docs: refresh PR 593 UI evidence callouts * refactor: deduplicate reasoning normalize functions and remove PR evidence - Export NormalizeReasoningEffort/NormalizeReasoningFallback from providers package; store package now delegates instead of duplicating - Store reasoning fallback constants alias providers canonical definitions - Export deriveLegacyThinkingLevel from types/provider.ts; remove local copies from agent-advanced-dialog and provider-overview - Remove unused _providerType param from useProviderModels hook - Fix reasoning debug log to fire for all cases with a reason (not just non-off efforts) - Remove docs/pr-593-evidence/ binary screenshots from repo --------- Co-authored-by: viettranx <viettranx@gmail.com>
This commit is contained in:
1 parent
1dee22aeb7
commit
3ca3bb2062
41 files changed
+2354
-213
No files matched your search
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/nextlevelbuilder/goclaw/internal/agent"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/config"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/edition"
|
||||
httpapi "github.com/nextlevelbuilder/goclaw/internal/http"
|
||||
kg "github.com/nextlevelbuilder/goclaw/internal/knowledgegraph"
|
||||
mcpbridge "github.com/nextlevelbuilder/goclaw/internal/mcp"
|
||||
@@ -21,7 +22,6 @@ import (
|
||||
"github.com/nextlevelbuilder/goclaw/internal/sandbox"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/skills"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/edition"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store/pg"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/tools"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/tracing"
|
||||
@@ -131,6 +131,7 @@ func wireExtras(
|
||||
|
||||
resolver := agent.NewManagedResolver(agent.ResolverDeps{
|
||||
AgentStore: stores.Agents,
|
||||
ProviderStore: stores.Providers,
|
||||
ProviderReg: providerReg,
|
||||
Bus: msgBus,
|
||||
Sessions: sessStore,
|
||||
@@ -601,4 +602,3 @@ func buildKGExtractFunc(kgStore store.KnowledgeGraphStore, bts store.BuiltinTool
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
-7
@@ -271,25 +271,25 @@ flowchart LR
|
||||
|
||||
## 8. Extended Thinking
|
||||
|
||||
Extended thinking allows LLMs to generate internal reasoning tokens before producing a response, improving quality for complex tasks. GoClaw supports this across multiple providers with a unified `thinking_level` configuration. See [12-extended-thinking.md](./12-extended-thinking.md) for full details.
|
||||
Extended thinking allows LLMs to generate internal reasoning tokens before producing a response, improving quality for complex tasks. GoClaw supports this across multiple providers with provider-owned reasoning defaults, agent inherit/custom overrides, and a legacy `thinking_level` shim for rollback compatibility. See [12-extended-thinking.md](./12-extended-thinking.md) for full details.
|
||||
|
||||
### Provider Mapping
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
LEVEL["thinking_level"] --> CHECK{"Provider<br/>supports thinking?"}
|
||||
LEVEL["provider.settings.reasoning_defaults<br/>+ agent other_config.reasoning"] --> CHECK{"Provider<br/>supports thinking?"}
|
||||
CHECK -->|No| SKIP["Skip — normal request"]
|
||||
CHECK -->|Yes| TYPE{"Provider type?"}
|
||||
|
||||
TYPE -->|Anthropic| ANTH["Budget tokens:<br/>low=4K, medium=10K, high=32K<br/>+ anthropic-beta header<br/>+ strip temperature"]
|
||||
TYPE -->|OpenAI-compat| OAI["reasoning_effort:<br/>low / medium / high"]
|
||||
TYPE -->|OpenAI-compat| OAI["capability-aware<br/>reasoning_effort"]
|
||||
TYPE -->|DashScope| DASH["enable_thinking: true<br/>Budget: low=4K, medium=16K, high=32K<br/>⚠ No streaming with tools"]
|
||||
```
|
||||
|
||||
### Streaming
|
||||
|
||||
- **Anthropic**: `thinking_delta` events accumulate into `StreamChunk.Thinking`
|
||||
- **OpenAI-compat**: `reasoning_content` in response delta
|
||||
- **OpenAI-compat**: `reasoning_content` in response delta, with GPT-5/Codex effort normalization when the model is known
|
||||
- **DashScope**: Falls back to non-streaming when tools are present, synthesizes chunk callbacks
|
||||
|
||||
### Tool Loop Handling
|
||||
@@ -621,7 +621,7 @@ Codex supports SSE streaming similar to Anthropic:
|
||||
|
||||
### Extended Thinking
|
||||
|
||||
Codex provider reports `SupportsThinking() = true`, allowing thinking_level to be injected. The provider maps thinking levels to reasoning_effort parameters as needed.
|
||||
Codex provider reports `SupportsThinking() = true`, allowing capability-aware reasoning effort injection. Providers can save reusable `settings.reasoning_defaults`, agents inherit them by default, and custom agent overrides remain additive. For known GPT-5/Codex models, GoClaw resolves requested versus effective effort before the request and records the source and outcome in trace metadata.
|
||||
|
||||
### Token Usage
|
||||
|
||||
@@ -646,15 +646,31 @@ Provider default example:
|
||||
}
|
||||
```
|
||||
|
||||
Provider reasoning default example:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "openai-codex",
|
||||
"provider_type": "chatgpt_oauth",
|
||||
"settings": {
|
||||
"reasoning_defaults": {
|
||||
"effort": "high",
|
||||
"fallback": "provider_default"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Agent override example:
|
||||
|
||||
```json
|
||||
{
|
||||
"provider": "openai-codex",
|
||||
"other_config": {
|
||||
"chatgpt_oauth_routing": {
|
||||
"reasoning": {
|
||||
"override_mode": "custom",
|
||||
"strategy": "round_robin"
|
||||
"effort": "xhigh",
|
||||
"fallback": "downgrade"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -674,6 +690,14 @@ Routing behavior:
|
||||
- Explicit provider names remain explicit. OAuth auth/logout is still provider-scoped.
|
||||
- Runtime observability for one agent is available at `GET /v1/agents/{id}/codex-pool-activity`, which exposes recent routed traces plus per-alias health derived from those traces.
|
||||
|
||||
Reasoning behavior:
|
||||
- `settings.reasoning_defaults` is provider-owned and reusable across agents.
|
||||
- `reasoning.override_mode: "inherit"` follows the provider default.
|
||||
- `reasoning.override_mode: "custom"` stores an agent-local reasoning policy.
|
||||
- Existing `reasoning` payloads without `override_mode` still behave as custom overrides.
|
||||
- If no provider default is saved, inherit resolves to reasoning `off`.
|
||||
- Trace metadata surfaces the reasoning `source` so provider-default behavior is no longer implicit.
|
||||
|
||||
---
|
||||
|
||||
## 14. File Reference
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
## Overview
|
||||
|
||||
Extended thinking allows LLM providers to "think out loud" before producing a final response. When enabled, the model generates internal reasoning tokens that improve response quality for complex tasks — at the cost of additional token usage and latency. GoClaw supports extended thinking across multiple providers with a unified `thinking_level` configuration.
|
||||
Extended thinking allows LLM providers to "think out loud" before producing a final response. When enabled, the model generates internal reasoning tokens that improve response quality for complex tasks at the cost of additional token usage and latency. GoClaw now supports both the legacy coarse `thinking_level` setting and a provider-first reasoning policy for capability-aware GPT-5/Codex control.
|
||||
|
||||
---
|
||||
|
||||
## 1. Configuration
|
||||
|
||||
Thinking is controlled per-agent through the `thinking_level` setting.
|
||||
The reusable default now lives on the provider in `settings.reasoning_defaults`. Agents consume that default by inheriting it, or store a custom override in `other_config.reasoning`. `thinking_level` remains the backward-compatible coarse shim for older builds.
|
||||
|
||||
| Level | Behavior |
|
||||
|-------|----------|
|
||||
@@ -17,26 +17,74 @@ Thinking is controlled per-agent through the `thinking_level` setting.
|
||||
| `medium` | Moderate thinking — balanced reasoning |
|
||||
| `high` | Maximum thinking — deep reasoning for complex tasks |
|
||||
|
||||
The setting can be configured:
|
||||
- **Per-agent**: In the agent's configuration (applies to all users of that agent)
|
||||
- **Per-user override**: Via `user_agent_overrides` table (reserved for future use)
|
||||
### Provider default
|
||||
|
||||
```json
|
||||
{
|
||||
"provider_type": "chatgpt_oauth",
|
||||
"settings": {
|
||||
"reasoning_defaults": {
|
||||
"effort": "high",
|
||||
"fallback": "provider_default"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Agent inherits provider default
|
||||
|
||||
```json
|
||||
{
|
||||
"other_config": {
|
||||
"reasoning": {
|
||||
"override_mode": "inherit"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Agent custom override
|
||||
|
||||
```json
|
||||
{
|
||||
"other_config": {
|
||||
"thinking_level": "high",
|
||||
"reasoning": {
|
||||
"override_mode": "custom",
|
||||
"effort": "xhigh",
|
||||
"fallback": "downgrade"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Unset provider defaults and unset agent reasoning both resolve to `off`.
|
||||
- `settings.reasoning_defaults` is provider-owned and reusable across agents.
|
||||
- `reasoning.override_mode` accepts `inherit|custom`.
|
||||
- `thinking_level` still accepts `off|low|medium|high`.
|
||||
- `reasoning.effort` accepts `off|auto|none|minimal|low|medium|high|xhigh`.
|
||||
- `reasoning.fallback` accepts `downgrade|off|provider_default`.
|
||||
- Existing `reasoning` payloads without `override_mode` are treated as custom overrides for backward compatibility.
|
||||
- Read path resolves provider defaults first, then applies agent inherit/custom semantics, then falls back to legacy `thinking_level`.
|
||||
- Write path keeps a derived coarse `thinking_level` only for custom agent overrides so rollback to older GoClaw builds stays safe.
|
||||
|
||||
---
|
||||
|
||||
## 2. Provider Support
|
||||
|
||||
Each provider maps the abstract `thinking_level` to its own implementation parameters.
|
||||
Each provider maps the normalized reasoning policy to its own implementation parameters.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
CONFIG["Agent config:<br/>thinking_level = medium"] --> CHECK{"Provider supports<br/>thinking?"}
|
||||
CONFIG["Provider defaults +<br/>agent inherit/custom"] --> CHECK{"Provider supports<br/>thinking?"}
|
||||
CHECK -->|No| SKIP["Send request<br/>without thinking"]
|
||||
CHECK -->|Yes| MAP{"Provider type?"}
|
||||
|
||||
MAP -->|Anthropic| ANTH["Budget tokens: 10,000<br/>Header: anthropic-beta<br/>Strip temperature"]
|
||||
MAP -->|OpenAI-compat| OAI["Map to reasoning_effort<br/>(low/medium/high)"]
|
||||
MAP -->|OpenAI-compat| OAI["Capability-aware effort<br/>or provider default"]
|
||||
MAP -->|DashScope| DASH["enable_thinking: true<br/>Budget: 16,384 tokens<br/>⚠ Model-specific + tools limitation"]
|
||||
MAP -->|Codex| CODEX["reasoning_tokens tracked<br/>via Responses API"]
|
||||
MAP -->|Codex| CODEX["Capability-aware effort<br/>+ trace metadata"]
|
||||
|
||||
ANTH --> SEND["Send to LLM"]
|
||||
OAI --> SEND
|
||||
@@ -58,14 +106,25 @@ When thinking is enabled:
|
||||
- Strips `temperature` parameter (Anthropic requirement — cannot use temperature with thinking)
|
||||
- Auto-adjusts `max_tokens` to accommodate thinking budget (budget + 8,192 buffer)
|
||||
|
||||
### OpenAI-Compatible (OpenAI, Groq, DeepSeek, etc.)
|
||||
### OpenAI-Compatible and Codex (GPT-5 / Codex families)
|
||||
|
||||
Maps `thinking_level` directly to `reasoning_effort`:
|
||||
- `low` → `reasoning_effort: "low"`
|
||||
- `medium` → `reasoning_effort: "medium"`
|
||||
- `high` → `reasoning_effort: "high"`
|
||||
Known GPT-5/Codex models use a static capability registry. The runtime resolves:
|
||||
- requested effort
|
||||
- actual effective effort
|
||||
- fallback policy used
|
||||
- whether the model default was used
|
||||
- whether the source was the provider default or an agent override
|
||||
|
||||
Reasoning content is returned in the `reasoning_content` field of the response delta during streaming.
|
||||
If the model is known:
|
||||
- supported efforts pass through unchanged
|
||||
- unsupported efforts are normalized via `downgrade`, `off`, or `provider_default`
|
||||
- `auto` means "use the model default effort"
|
||||
|
||||
If the model is unknown:
|
||||
- explicit non-`auto` effort is passed through as requested
|
||||
- `auto` leaves provider-default reasoning untouched
|
||||
|
||||
Reasoning content still streams in the provider-native format, and span metadata now records the source plus requested versus effective effort.
|
||||
|
||||
### DashScope (Alibaba Qwen)
|
||||
|
||||
@@ -86,12 +145,14 @@ Other models (e.g., `qwen3-plus`, `qwen3-turbo`) silently skip thinking injectio
|
||||
|
||||
**Important limitation**: DashScope does not support streaming when tools are present. When an agent has tools enabled and thinking is active, the provider automatically falls back to non-streaming mode (single `Chat()` call) and synthesizes chunk callbacks to maintain the event flow.
|
||||
|
||||
### Codex (Alibaba AI Reasoning)
|
||||
### Codex (ChatGPT OAuth Responses API)
|
||||
|
||||
Codex natively supports extended reasoning through its Responses API. Thinking/reasoning tokens are streamed as discrete `reasoning` events with summary fragments.
|
||||
Codex natively supports extended reasoning through its Responses API. Thinking and reasoning tokens are streamed as discrete `reasoning` events with summary fragments.
|
||||
|
||||
**Token tracking**: Reasoning token count is exposed in `response.completed` / `response.incomplete` events as `OutputTokensDetails.ReasoningTokens` and accessible via `ChatResponse.Usage.ThinkingTokens`.
|
||||
|
||||
**Model metadata**: `/v1/providers/{id}/models` is now the backend source of truth for the ChatGPT OAuth model list and any known reasoning capabilities.
|
||||
|
||||
---
|
||||
|
||||
## 3. Streaming
|
||||
@@ -162,6 +223,22 @@ OpenAI-compatible providers handle thinking/reasoning content as metadata. The `
|
||||
| Anthropic | Temperature parameter stripped when thinking is enabled |
|
||||
| All | Thinking tokens count against the context window budget |
|
||||
| All | Thinking increases latency and cost proportional to the budget level |
|
||||
| GPT-5/Codex unknown models | GoClaw allows explicit effort passthrough but does not claim a capability contract |
|
||||
|
||||
---
|
||||
|
||||
## 6. Observability
|
||||
|
||||
Each LLM span can now include a `metadata.reasoning` section with:
|
||||
- `source`
|
||||
- `requested_effort`
|
||||
- `effective_effort`
|
||||
- `fallback`
|
||||
- `reason`
|
||||
- `supported_levels`
|
||||
- `used_provider_default`
|
||||
|
||||
This makes silent downgrades or provider-default decisions visible in traces instead of leaving them implicit.
|
||||
|
||||
---
|
||||
|
||||
@@ -174,6 +251,9 @@ OpenAI-compatible providers handle thinking/reasoning content as metadata. The `
|
||||
| `internal/providers/anthropic_stream.go` | Anthropic streaming: thinking_delta handling, raw block accumulation |
|
||||
| `internal/providers/anthropic_request.go` | Anthropic request: thinking block preservation for tool loops |
|
||||
| `internal/providers/openai.go` | OpenAI-compat: reasoning_effort mapping, reasoning_content streaming |
|
||||
| `internal/providers/reasoning_capability.go` | Static GPT-5/Codex capability registry |
|
||||
| `internal/providers/reasoning_resolution.go` | Requested-to-effective reasoning decision engine |
|
||||
| `internal/providers/reasoning_observation.go` | Trace metadata merge helpers for reasoning decisions |
|
||||
| `internal/providers/dashscope.go` | DashScope: model-specific thinking guard, budget mapping, tools+streaming fallback |
|
||||
| `internal/providers/codex.go` | Codex: reasoning event streaming, OutputTokensDetails.ReasoningTokens tracking |
|
||||
|
||||
|
||||
+82
-1
@@ -185,6 +185,55 @@ Provider-level defaults example:
|
||||
}
|
||||
```
|
||||
|
||||
### Provider reasoning defaults in `settings`
|
||||
|
||||
Providers can store the reusable default reasoning policy in `settings.reasoning_defaults`.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "openai-codex",
|
||||
"provider_type": "chatgpt_oauth",
|
||||
"settings": {
|
||||
"reasoning_defaults": {
|
||||
"effort": "high",
|
||||
"fallback": "provider_default"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
- these defaults are provider-owned and apply to any agent that inherits reasoning from this provider
|
||||
- the final runtime effort is still normalized against the agent's selected model capabilities
|
||||
- if no provider default is saved, inherit mode resolves to reasoning `off`
|
||||
|
||||
### Agent reasoning policy in `other_config`
|
||||
|
||||
Agents can now store capability-aware GPT-5/Codex reasoning intent under `other_config.reasoning`.
|
||||
|
||||
```json
|
||||
{
|
||||
"provider": "openai-codex",
|
||||
"model": "gpt-5.4",
|
||||
"other_config": {
|
||||
"reasoning": {
|
||||
"override_mode": "inherit"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
- `reasoning.override_mode` supports `inherit|custom`
|
||||
- `override_mode: "inherit"` tells the agent to follow `settings.reasoning_defaults`
|
||||
- `override_mode: "custom"` stores an agent-local override; the dashboard also writes a derived `thinking_level` shim for rollback safety
|
||||
- `thinking_level` remains the coarse compatibility shim: `off|low|medium|high`
|
||||
- `reasoning.effort` supports `off|auto|none|minimal|low|medium|high|xhigh`
|
||||
- `reasoning.fallback` supports `downgrade|off|provider_default`
|
||||
- existing `reasoning` payloads without `override_mode` continue to behave as custom overrides
|
||||
- unset reasoning resolves to `off`
|
||||
- the runtime may normalize unsupported efforts, and the actual decision is surfaced in trace span metadata
|
||||
|
||||
### Codex Pool Activity
|
||||
|
||||
| Method | Path | Description |
|
||||
@@ -273,11 +322,43 @@ LLM provider management. API keys are encrypted with AES-256-GCM in the database
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `POST` | `/v1/providers/{id}/verify` | Test provider+model with minimal LLM call |
|
||||
| `GET` | `/v1/providers/{id}/models` | Proxy to upstream provider model list |
|
||||
| `GET` | `/v1/providers/{id}/models` | List models plus any known reasoning capability metadata |
|
||||
| `GET` | `/v1/providers/claude-cli/auth-status` | Check Claude CLI login status |
|
||||
|
||||
**Supported types:** `anthropic_native`, `openai_compat`, `chatgpt_oauth`, `gemini_native`, `dashscope`, `bailian`, `minimax`, `claude_cli`, `acp`
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"models": [
|
||||
{
|
||||
"id": "gpt-5.4",
|
||||
"name": "GPT-5.4",
|
||||
"reasoning": {
|
||||
"levels": ["none", "low", "medium", "high", "xhigh"],
|
||||
"default_effort": "none"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "custom-model",
|
||||
"name": "custom-model"
|
||||
}
|
||||
],
|
||||
"reasoning_defaults": {
|
||||
"effort": "high",
|
||||
"fallback": "provider_default"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `chatgpt_oauth` providers return a backend-owned model list because OAuth tokens cannot rely on `/v1/models`
|
||||
- `reasoning_defaults` is returned only when the provider has saved defaults and at least one returned model exposes reasoning capability metadata
|
||||
- unknown models remain usable and simply omit the `reasoning` field
|
||||
- the web UI uses this endpoint as the source of truth for provider-first reasoning controls
|
||||
- when upstream model discovery fails, the endpoint returns an empty `models` array instead of a hard error
|
||||
|
||||
---
|
||||
|
||||
## 7. Sessions
|
||||
|
||||
+20
-7
@@ -262,13 +262,23 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (result *RunResult,
|
||||
if tid := store.TenantIDFromContext(ctx); tid != uuid.Nil {
|
||||
chatReq.Options[providers.OptTenantID] = tid.String()
|
||||
}
|
||||
if l.thinkingLevel != "" && l.thinkingLevel != "off" {
|
||||
if tc, ok := provider.(providers.ThinkingCapable); ok && tc.SupportsThinking() {
|
||||
chatReq.Options[providers.OptThinkingLevel] = l.thinkingLevel
|
||||
} else {
|
||||
slog.Debug("thinking_level ignored: provider does not support thinking",
|
||||
"provider", provider.Name(), "level", l.thinkingLevel)
|
||||
}
|
||||
reasoningDecision := providers.ResolveReasoningDecision(
|
||||
provider,
|
||||
model,
|
||||
l.reasoningConfig.Effort,
|
||||
l.reasoningConfig.Fallback,
|
||||
l.reasoningConfig.Source,
|
||||
)
|
||||
if effort := reasoningDecision.RequestEffort(); effort != "" {
|
||||
chatReq.Options[providers.OptThinkingLevel] = effort
|
||||
}
|
||||
if reasoningDecision.Reason != "" {
|
||||
slog.Debug("reasoning normalized",
|
||||
"provider", provider.Name(),
|
||||
"model", model,
|
||||
"requested", reasoningDecision.RequestedEffort,
|
||||
"effective", reasoningDecision.EffectiveEffort,
|
||||
"reason", reasoningDecision.Reason)
|
||||
}
|
||||
|
||||
// Call LLM (streaming or non-streaming)
|
||||
@@ -276,6 +286,9 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (result *RunResult,
|
||||
var err error
|
||||
|
||||
callCtx := providers.WithChatGPTOAuthRoutingObservation(ctx, providers.NewChatGPTOAuthRoutingObservation())
|
||||
if reasoningDecision.HasObservation() {
|
||||
callCtx = providers.WithReasoningDecision(callCtx, reasoningDecision)
|
||||
}
|
||||
llmSpanStart := time.Now().UTC()
|
||||
llmSpanID := l.emitLLMSpanStart(callCtx, llmSpanStart, rs.iteration, messages)
|
||||
|
||||
|
||||
@@ -162,6 +162,9 @@ func (l *Loop) emitLLMSpanEnd(ctx context.Context, spanID uuid.UUID, start time.
|
||||
}
|
||||
}
|
||||
}
|
||||
if decision := providers.ReasoningDecisionFromContext(ctx); decision != nil {
|
||||
spanMetadata = providers.MergeReasoningMetadata(spanMetadata, *decision)
|
||||
}
|
||||
if len(spanMetadata) > 0 {
|
||||
updates["metadata"] = spanMetadata
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ const bootstrapAutoCleanupTurns = 3
|
||||
// Consolidates workspace resolution and context file seeding into one struct
|
||||
// to prevent desync between the two concerns.
|
||||
type userSetup struct {
|
||||
workspace string // effective workspace from user_agent_profiles (expanded, absolute)
|
||||
seeded bool // whether SeedUserFiles has been called this instance
|
||||
fallbackBootstrap []bootstrap.ContextFile // in-memory fallback when DB seed fails (e.g. SQLITE_BUSY)
|
||||
workspace string // effective workspace from user_agent_profiles (expanded, absolute)
|
||||
seeded bool // whether SeedUserFiles has been called this instance
|
||||
fallbackBootstrap []bootstrap.ContextFile // in-memory fallback when DB seed fails (e.g. SQLITE_BUSY)
|
||||
}
|
||||
|
||||
// EnsureUserProfileFunc creates/resolves a user's profile and workspace.
|
||||
@@ -65,25 +65,25 @@ type CacheInvalidateFunc func(agentID uuid.UUID, userID string)
|
||||
// Loop is the agent execution loop for one agent instance.
|
||||
// Think → Act → Observe cycle with tool execution.
|
||||
type Loop struct {
|
||||
id string
|
||||
agentUUID uuid.UUID // set for context propagation
|
||||
tenantID uuid.UUID // agent's owning tenant
|
||||
agentType string // "open" or "predefined"
|
||||
provider providers.Provider
|
||||
model string
|
||||
contextWindow int
|
||||
maxTokens int // max output tokens per LLM call (0 = default 8192)
|
||||
maxIterations int
|
||||
maxToolCalls int
|
||||
id string
|
||||
agentUUID uuid.UUID // set for context propagation
|
||||
tenantID uuid.UUID // agent's owning tenant
|
||||
agentType string // "open" or "predefined"
|
||||
provider providers.Provider
|
||||
model string
|
||||
contextWindow int
|
||||
maxTokens int // max output tokens per LLM call (0 = default 8192)
|
||||
maxIterations int
|
||||
maxToolCalls int
|
||||
workspace string
|
||||
dataDir string // global workspace root for team workspace resolution
|
||||
workspaceSharing *store.WorkspaceSharingConfig
|
||||
|
||||
// Per-agent overrides from DB (nil = use global defaults)
|
||||
restrictToWs *bool
|
||||
subagentsCfg *config.SubagentsConfig
|
||||
memoryCfg *config.MemoryConfig
|
||||
sandboxCfg *sandbox.Config
|
||||
restrictToWs *bool
|
||||
subagentsCfg *config.SubagentsConfig
|
||||
memoryCfg *config.MemoryConfig
|
||||
sandboxCfg *sandbox.Config
|
||||
|
||||
eventPub bus.EventPublisher // currently unused by Loop; kept for future use
|
||||
sessions store.SessionStore
|
||||
@@ -108,14 +108,14 @@ type Loop struct {
|
||||
ensureUserFiles EnsureUserFilesFunc // legacy combined callback (fallback)
|
||||
contextFileLoader ContextFileLoaderFunc
|
||||
bootstrapCleanup BootstrapCleanupFunc
|
||||
cacheInvalidate CacheInvalidateFunc // invalidate context file cache after seeding
|
||||
userSetups sync.Map // userID → *userSetup (workspace + seeding state, per Loop instance)
|
||||
cacheInvalidate CacheInvalidateFunc // invalidate context file cache after seeding
|
||||
userSetups sync.Map // userID → *userSetup (workspace + seeding state, per Loop instance)
|
||||
|
||||
// Per-user MCP tools: servers requiring user credentials get connected per-request.
|
||||
mcpStore store.MCPServerStore // for credential lookup
|
||||
mcpPool *mcpbridge.Pool // user-keyed connection pool
|
||||
mcpUserCredSrvs []store.MCPAccessInfo // servers needing per-user creds
|
||||
mcpUserTools sync.Map // userID → []tools.Tool (cached per-user tools)
|
||||
mcpStore store.MCPServerStore // for credential lookup
|
||||
mcpPool *mcpbridge.Pool // user-keyed connection pool
|
||||
mcpUserCredSrvs []store.MCPAccessInfo // servers needing per-user creds
|
||||
mcpUserTools sync.Map // userID → []tools.Tool (cached per-user tools)
|
||||
|
||||
// Compaction config (memory flush settings)
|
||||
compactionCfg *config.CompactionConfig
|
||||
@@ -148,8 +148,8 @@ type Loop struct {
|
||||
// Per-tenant disabled tools (tool name → true means excluded from LLM)
|
||||
disabledTools map[string]bool
|
||||
|
||||
// Thinking level for extended thinking support
|
||||
thinkingLevel string
|
||||
// Requested reasoning config parsed from agent other_config.
|
||||
reasoningConfig store.AgentReasoningConfig
|
||||
|
||||
// Self-evolve: predefined agents can update SOUL.md through chat
|
||||
selfEvolve bool
|
||||
@@ -176,7 +176,7 @@ type Loop struct {
|
||||
|
||||
// Budget enforcement: monthly spending limit in cents (0 = unlimited)
|
||||
budgetMonthlyCents int
|
||||
tracingStore store.TracingStore
|
||||
tracingStore store.TracingStore
|
||||
|
||||
// Memory store for extractive memory fallback (writes directly when LLM flush fails)
|
||||
memStore store.MemoryStore
|
||||
@@ -208,13 +208,13 @@ type AgentEvent struct {
|
||||
|
||||
// LoopConfig configures a new Loop.
|
||||
type LoopConfig struct {
|
||||
ID string
|
||||
Provider providers.Provider
|
||||
Model string
|
||||
ContextWindow int
|
||||
MaxTokens int // max output tokens per LLM call (0 = default 8192)
|
||||
MaxIterations int
|
||||
MaxToolCalls int
|
||||
ID string
|
||||
Provider providers.Provider
|
||||
Model string
|
||||
ContextWindow int
|
||||
MaxTokens int // max output tokens per LLM call (0 = default 8192)
|
||||
MaxIterations int
|
||||
MaxToolCalls int
|
||||
Workspace string
|
||||
DataDir string // global workspace root for team workspace resolution
|
||||
WorkspaceSharing *store.WorkspaceSharingConfig
|
||||
@@ -264,7 +264,7 @@ type LoopConfig struct {
|
||||
EnsureUserFiles EnsureUserFilesFunc // legacy: combined (used when above are nil)
|
||||
ContextFileLoader ContextFileLoaderFunc
|
||||
BootstrapCleanup BootstrapCleanupFunc
|
||||
CacheInvalidate CacheInvalidateFunc // invalidate context file cache after seeding
|
||||
CacheInvalidate CacheInvalidateFunc // invalidate context file cache after seeding
|
||||
|
||||
// Tracing collector (nil = no tracing)
|
||||
TraceCollector *tracing.Collector
|
||||
@@ -280,8 +280,8 @@ type LoopConfig struct {
|
||||
// Per-tenant disabled tools (tool name → true means excluded)
|
||||
DisabledTools map[string]bool
|
||||
|
||||
// Thinking level: "off", "low", "medium", "high" (from agent other_config)
|
||||
ThinkingLevel string
|
||||
// Requested reasoning config parsed from agent other_config.
|
||||
ReasoningConfig store.AgentReasoningConfig
|
||||
|
||||
// Self-evolve: predefined agents can update SOUL.md (style/tone) through chat
|
||||
SelfEvolve bool
|
||||
@@ -307,15 +307,15 @@ type LoopConfig struct {
|
||||
|
||||
// Budget enforcement
|
||||
BudgetMonthlyCents int
|
||||
TracingStore store.TracingStore
|
||||
TracingStore store.TracingStore
|
||||
|
||||
// Memory store for extractive memory fallback (writes directly when LLM flush fails)
|
||||
MemoryStore store.MemoryStore
|
||||
|
||||
// Per-user MCP tools (servers requiring per-user credentials)
|
||||
MCPStore store.MCPServerStore // for credential lookup
|
||||
MCPPool *mcpbridge.Pool // user-keyed connection pool
|
||||
MCPUserCredSrvs []store.MCPAccessInfo // servers needing per-user creds
|
||||
MCPStore store.MCPServerStore // for credential lookup
|
||||
MCPPool *mcpbridge.Pool // user-keyed connection pool
|
||||
MCPUserCredSrvs []store.MCPAccessInfo // servers needing per-user creds
|
||||
}
|
||||
|
||||
const defaultMaxTokens = config.DefaultMaxTokens
|
||||
@@ -398,7 +398,7 @@ func NewLoop(cfg LoopConfig) *Loop {
|
||||
maxMessageChars: cfg.MaxMessageChars,
|
||||
builtinToolSettings: cfg.BuiltinToolSettings,
|
||||
disabledTools: cfg.DisabledTools,
|
||||
thinkingLevel: cfg.ThinkingLevel,
|
||||
reasoningConfig: cfg.ReasoningConfig,
|
||||
selfEvolve: cfg.SelfEvolve,
|
||||
skillEvolve: cfg.SkillEvolve,
|
||||
skillNudgeInterval: cfg.SkillNudgeInterval,
|
||||
@@ -418,33 +418,33 @@ func NewLoop(cfg LoopConfig) *Loop {
|
||||
|
||||
// RunRequest is the input for processing a message through the agent.
|
||||
type RunRequest struct {
|
||||
SessionKey string // composite key: agent:{agentId}:{channel}:{peerKind}:{chatId}
|
||||
Message string // user message
|
||||
Media []bus.MediaFile // local media files with MIME types
|
||||
ForwardMedia []bus.MediaFile // media files to forward to output (from delegation results)
|
||||
Channel string // source channel instance name (e.g. "my-telegram-bot")
|
||||
ChannelType string // platform type (e.g. "zalo_personal", "telegram") — for system prompt context
|
||||
ChatTitle string // group chat display name (e.g. Telegram group title)
|
||||
ChatID string // source chat ID
|
||||
PeerKind string // "direct" or "group" (for session key building and tool context)
|
||||
RunID string // unique run identifier
|
||||
UserID string // external user ID (TEXT, free-form) for multi-tenant scoping
|
||||
SenderID string // original individual sender ID (preserved in group chats for permission checks)
|
||||
Stream bool // whether to stream response chunks
|
||||
ExtraSystemPrompt string // optional: injected into system prompt (skills, subagent context, etc.)
|
||||
SkillFilter []string // per-request skill override: nil=use agent default, []=no skills, ["x","y"]=whitelist
|
||||
HistoryLimit int // max user turns to keep in context (0=unlimited, from channel config)
|
||||
ToolAllow []string // per-group tool allow list (nil = no restriction, supports "group:xxx")
|
||||
LocalKey string // composite key with topic/thread suffix for routing (e.g. "-100123:topic:42")
|
||||
ParentTraceID uuid.UUID // if set, reuse parent trace instead of creating new (announce runs)
|
||||
ParentRootSpanID uuid.UUID // if set, nest announce agent span under this parent span
|
||||
LinkedTraceID uuid.UUID // if set, create new trace with parent_trace_id pointing to this (team task runs)
|
||||
TraceName string // override trace name (default: "chat <agentID>")
|
||||
TraceTags []string // additional tags for the trace (e.g. "cron")
|
||||
MaxIterations int // per-request override (0 = use agent default, must be lower)
|
||||
ModelOverride string // per-request model override (heartbeat uses cheaper model)
|
||||
ProviderOverride providers.Provider // per-request provider override (heartbeat uses different provider)
|
||||
LightContext bool // skip loading context files (only inject ExtraSystemPrompt)
|
||||
SessionKey string // composite key: agent:{agentId}:{channel}:{peerKind}:{chatId}
|
||||
Message string // user message
|
||||
Media []bus.MediaFile // local media files with MIME types
|
||||
ForwardMedia []bus.MediaFile // media files to forward to output (from delegation results)
|
||||
Channel string // source channel instance name (e.g. "my-telegram-bot")
|
||||
ChannelType string // platform type (e.g. "zalo_personal", "telegram") — for system prompt context
|
||||
ChatTitle string // group chat display name (e.g. Telegram group title)
|
||||
ChatID string // source chat ID
|
||||
PeerKind string // "direct" or "group" (for session key building and tool context)
|
||||
RunID string // unique run identifier
|
||||
UserID string // external user ID (TEXT, free-form) for multi-tenant scoping
|
||||
SenderID string // original individual sender ID (preserved in group chats for permission checks)
|
||||
Stream bool // whether to stream response chunks
|
||||
ExtraSystemPrompt string // optional: injected into system prompt (skills, subagent context, etc.)
|
||||
SkillFilter []string // per-request skill override: nil=use agent default, []=no skills, ["x","y"]=whitelist
|
||||
HistoryLimit int // max user turns to keep in context (0=unlimited, from channel config)
|
||||
ToolAllow []string // per-group tool allow list (nil = no restriction, supports "group:xxx")
|
||||
LocalKey string // composite key with topic/thread suffix for routing (e.g. "-100123:topic:42")
|
||||
ParentTraceID uuid.UUID // if set, reuse parent trace instead of creating new (announce runs)
|
||||
ParentRootSpanID uuid.UUID // if set, nest announce agent span under this parent span
|
||||
LinkedTraceID uuid.UUID // if set, create new trace with parent_trace_id pointing to this (team task runs)
|
||||
TraceName string // override trace name (default: "chat <agentID>")
|
||||
TraceTags []string // additional tags for the trace (e.g. "cron")
|
||||
MaxIterations int // per-request override (0 = use agent default, must be lower)
|
||||
ModelOverride string // per-request model override (heartbeat uses cheaper model)
|
||||
ProviderOverride providers.Provider // per-request provider override (heartbeat uses different provider)
|
||||
LightContext bool // skip loading context files (only inject ExtraSystemPrompt)
|
||||
|
||||
// Run classification
|
||||
RunKind string // "delegation", "announce" — empty for user-initiated runs
|
||||
@@ -488,7 +488,7 @@ type RunResult struct {
|
||||
type MediaResult struct {
|
||||
Path string `json:"path"` // local file path
|
||||
ContentType string `json:"content_type,omitempty"` // MIME type
|
||||
Size int64 `json:"size,omitempty"` // file size in bytes
|
||||
Size int64 `json:"size,omitempty"` // file size in bytes
|
||||
AsVoice bool `json:"as_voice,omitempty"` // send as voice message (Telegram OGG)
|
||||
}
|
||||
|
||||
@@ -497,17 +497,17 @@ type MediaResult struct {
|
||||
// on *runState without passing 20+ individual variables.
|
||||
type runState struct {
|
||||
// Loop control
|
||||
loopDetector toolLoopState
|
||||
totalUsage providers.Usage
|
||||
iteration int
|
||||
loopDetector toolLoopState
|
||||
totalUsage providers.Usage
|
||||
iteration int
|
||||
totalToolCalls int
|
||||
|
||||
// Output accumulators
|
||||
finalContent string
|
||||
finalThinking string
|
||||
asyncToolCalls []string // async spawn tool names for fallback
|
||||
asyncToolCalls []string // async spawn tool names for fallback
|
||||
mediaResults []MediaResult
|
||||
deliverables []string // tool output content for team task results
|
||||
deliverables []string // tool output content for team task results
|
||||
pendingMsgs []providers.Message
|
||||
|
||||
// Event state
|
||||
@@ -520,7 +520,7 @@ type runState struct {
|
||||
// Mid-loop compaction and overhead calibration
|
||||
midLoopCompacted bool
|
||||
midLoopPruned bool
|
||||
overheadTokens int // non-history token overhead (system prompt + tools + context files)
|
||||
overheadTokens int // non-history token overhead (system prompt + tools + context files)
|
||||
overheadCalibrated bool
|
||||
|
||||
// Bootstrap detection
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
// ResolverDeps holds shared dependencies for the agent resolver.
|
||||
type ResolverDeps struct {
|
||||
AgentStore store.AgentStore
|
||||
ProviderStore store.ProviderStore
|
||||
ProviderReg *providers.Registry
|
||||
Bus bus.EventPublisher
|
||||
Sessions store.SessionStore
|
||||
@@ -134,9 +135,9 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc {
|
||||
provider, _ = deps.ProviderReg.GetForTenant(ag.TenantID, names[0])
|
||||
slog.Warn("agent provider not found, using fallback",
|
||||
"agent", agentKey, "wanted", ag.Provider, "using", names[0])
|
||||
if tl := ag.ParseThinkingLevel(); tl != "" && tl != "off" {
|
||||
if rc := ag.ParseReasoningConfig(); rc.Effort != "" && rc.Effort != "off" {
|
||||
slog.Warn("agent thinking may not be supported by fallback provider",
|
||||
"agent", agentKey, "thinking_level", tl,
|
||||
"agent", agentKey, "thinking_level", rc.Effort,
|
||||
"wanted_provider", ag.Provider, "fallback_provider", names[0])
|
||||
}
|
||||
}
|
||||
@@ -144,6 +145,12 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc {
|
||||
if provider == nil {
|
||||
return nil, fmt.Errorf("no provider available for agent %s", agentKey)
|
||||
}
|
||||
providerReasoningDefaults := (*store.ProviderReasoningConfig)(nil)
|
||||
if deps.ProviderStore != nil {
|
||||
if providerData, err := deps.ProviderStore.GetProviderByName(ctx, provider.Name()); err == nil && providerData != nil {
|
||||
providerReasoningDefaults = store.ParseProviderReasoningConfig(providerData.Settings)
|
||||
}
|
||||
}
|
||||
|
||||
// Load bootstrap files from DB
|
||||
contextFiles := bootstrap.LoadFromStore(ctx, deps.AgentStore, ag.ID)
|
||||
@@ -392,7 +399,7 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc {
|
||||
SandboxWorkspaceAccess: sandboxWorkspaceAccess,
|
||||
BuiltinToolSettings: builtinSettings,
|
||||
DisabledTools: disabledTools,
|
||||
ThinkingLevel: ag.ParseThinkingLevel(),
|
||||
ReasoningConfig: store.ResolveEffectiveReasoningConfig(providerReasoningDefaults, ag.ParseReasoningConfig()),
|
||||
SelfEvolve: ag.ParseSelfEvolve(),
|
||||
SkillEvolve: ag.AgentType == store.AgentTypePredefined && ag.ParseSkillEvolve(),
|
||||
SkillNudgeInterval: ag.ParseSkillNudgeInterval(),
|
||||
|
||||
+114
-10
@@ -487,7 +487,56 @@
|
||||
"tags": ["Providers"],
|
||||
"summary": "List available models",
|
||||
"parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
|
||||
"responses": { "200": { "description": "Model list" } }
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Model list with optional reasoning capability metadata",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"models": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"reasoning": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"levels": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"default_effort": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["id"]
|
||||
}
|
||||
},
|
||||
"reasoning_defaults": {
|
||||
"type": "object",
|
||||
"description": "Provider-owned default reasoning policy returned when the provider has saved defaults and at least one discovered model exposes reasoning capability metadata.",
|
||||
"properties": {
|
||||
"effort": {
|
||||
"type": "string",
|
||||
"enum": ["off", "auto", "none", "minimal", "low", "medium", "high", "xhigh"]
|
||||
},
|
||||
"fallback": {
|
||||
"type": "string",
|
||||
"enum": ["downgrade", "off", "provider_default"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["models"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/providers/{id}/verify": {
|
||||
@@ -837,14 +886,37 @@
|
||||
"description": { "type": "string" },
|
||||
"provider": { "type": "string", "description": "LLM provider name" },
|
||||
"model": { "type": "string", "description": "Model ID" },
|
||||
"system_prompt": { "type": "string" },
|
||||
"other_config": {
|
||||
"type": "object",
|
||||
"description": "Optional per-agent JSON config.",
|
||||
"properties": {
|
||||
"chatgpt_oauth_routing": {
|
||||
"type": "object",
|
||||
"description": "Optional agent-side routing override for ChatGPT OAuth providers. The main provider field remains the preferred/default account, while provider settings may supply inherited defaults.",
|
||||
"system_prompt": { "type": "string" },
|
||||
"other_config": {
|
||||
"type": "object",
|
||||
"description": "Optional per-agent JSON config.",
|
||||
"properties": {
|
||||
"thinking_level": {
|
||||
"type": "string",
|
||||
"enum": ["off", "low", "medium", "high"],
|
||||
"description": "Legacy coarse reasoning shim. Unset means off."
|
||||
},
|
||||
"reasoning": {
|
||||
"type": "object",
|
||||
"description": "Capability-aware reasoning policy for GPT-5/Codex models.",
|
||||
"properties": {
|
||||
"override_mode": {
|
||||
"type": "string",
|
||||
"enum": ["inherit", "custom"]
|
||||
},
|
||||
"effort": {
|
||||
"type": "string",
|
||||
"enum": ["off", "auto", "none", "minimal", "low", "medium", "high", "xhigh"]
|
||||
},
|
||||
"fallback": {
|
||||
"type": "string",
|
||||
"enum": ["downgrade", "off", "provider_default"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"chatgpt_oauth_routing": {
|
||||
"type": "object",
|
||||
"description": "Optional agent-side routing override for ChatGPT OAuth providers. The main provider field remains the preferred/default account, while provider settings may supply inherited defaults.",
|
||||
"properties": {
|
||||
"override_mode": { "type": "string", "enum": ["inherit", "custom"] },
|
||||
"strategy": { "type": "string", "enum": ["manual", "primary_first", "round_robin", "priority_order"] },
|
||||
@@ -864,7 +936,39 @@
|
||||
"provider_type": { "type": "string", "enum": ["anthropic_native", "openai_compat", "gemini_native", "openrouter", "groq", "deepseek", "mistral", "xai", "minimax_native", "cohere", "perplexity", "dashscope", "bailian", "chatgpt_oauth", "claude_cli", "suno", "yescale", "zai", "zai_coding", "ollama", "ollama_cloud", "acp"] },
|
||||
"api_base": { "type": "string" },
|
||||
"api_key": { "type": "string" },
|
||||
"enabled": { "type": "boolean", "default": true }
|
||||
"enabled": { "type": "boolean", "default": true },
|
||||
"settings": {
|
||||
"type": "object",
|
||||
"description": "Provider-specific settings such as provider-owned Codex pool defaults and reasoning defaults.",
|
||||
"properties": {
|
||||
"reasoning_defaults": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"effort": {
|
||||
"type": "string",
|
||||
"enum": ["off", "auto", "none", "minimal", "low", "medium", "high", "xhigh"]
|
||||
},
|
||||
"fallback": {
|
||||
"type": "string",
|
||||
"enum": ["downgrade", "off", "provider_default"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"codex_pool": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"strategy": {
|
||||
"type": "string",
|
||||
"enum": ["manual", "primary_first", "round_robin", "priority_order"]
|
||||
},
|
||||
"extra_provider_names": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Error": {
|
||||
|
||||
@@ -12,13 +12,20 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
// ModelInfo is a normalized model entry returned by the list-models endpoint.
|
||||
type ModelInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Reasoning *providers.ReasoningCapability `json:"reasoning,omitempty"`
|
||||
}
|
||||
|
||||
type ProviderModelsResponse struct {
|
||||
Models []ModelInfo `json:"models"`
|
||||
ReasoningDefaults *store.ProviderReasoningConfig `json:"reasoning_defaults,omitempty"`
|
||||
}
|
||||
|
||||
// handleListProviderModels proxies to the upstream provider API to list
|
||||
@@ -39,15 +46,27 @@ func (h *ProvidersHandler) handleListProviderModels(w http.ResponseWriter, r *ht
|
||||
return
|
||||
}
|
||||
|
||||
respond := func(models []ModelInfo) {
|
||||
writeJSON(w, http.StatusOK, ProviderModelsResponse{
|
||||
Models: models,
|
||||
ReasoningDefaults: reasoningDefaultsForModels(p.Settings, models),
|
||||
})
|
||||
}
|
||||
|
||||
// Claude CLI doesn't need an API key — return hardcoded models
|
||||
if p.ProviderType == store.ProviderClaudeCLI {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"models": claudeCLIModels()})
|
||||
respond(claudeCLIModels())
|
||||
return
|
||||
}
|
||||
|
||||
if p.ProviderType == store.ProviderChatGPTOAuth {
|
||||
respond(chatGPTOAuthModels())
|
||||
return
|
||||
}
|
||||
|
||||
// ACP agents don't need an API key — return hardcoded models
|
||||
if p.ProviderType == store.ProviderACP {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"models": acpModels()})
|
||||
respond(acpModels())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -86,11 +105,26 @@ func (h *ProvidersHandler) handleListProviderModels(w http.ResponseWriter, r *ht
|
||||
if err != nil {
|
||||
slog.Warn("providers.models", "provider", p.Name, "error", err)
|
||||
// Return empty list instead of error — provider may not support /models
|
||||
writeJSON(w, http.StatusOK, map[string]any{"models": []ModelInfo{}})
|
||||
respond([]ModelInfo{})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{"models": models})
|
||||
respond(withReasoningCapabilities(models))
|
||||
}
|
||||
|
||||
func reasoningDefaultsForModels(
|
||||
settings []byte,
|
||||
models []ModelInfo,
|
||||
) *store.ProviderReasoningConfig {
|
||||
if len(models) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, model := range models {
|
||||
if model.Reasoning != nil {
|
||||
return store.ParseProviderReasoningConfig(settings)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchAnthropicModels calls the Anthropic models API.
|
||||
@@ -293,3 +327,28 @@ func fetchOpenAIModels(ctx context.Context, apiBase, apiKey string) ([]ModelInfo
|
||||
}
|
||||
return models, nil
|
||||
}
|
||||
|
||||
func chatGPTOAuthModels() []ModelInfo {
|
||||
return withReasoningCapabilities([]ModelInfo{
|
||||
{ID: "gpt-5.4", Name: "GPT-5.4"},
|
||||
{ID: "gpt-5.4-mini", Name: "GPT-5.4 Mini"},
|
||||
{ID: "gpt-5.3-codex", Name: "GPT-5.3 Codex"},
|
||||
{ID: "gpt-5.3-codex-spark", Name: "GPT-5.3 Codex Spark"},
|
||||
{ID: "gpt-5.2-codex", Name: "GPT-5.2 Codex"},
|
||||
{ID: "gpt-5.2", Name: "GPT-5.2"},
|
||||
{ID: "gpt-5.1-codex", Name: "GPT-5.1 Codex"},
|
||||
{ID: "gpt-5.1-codex-max", Name: "GPT-5.1 Codex Max"},
|
||||
{ID: "gpt-5.1-codex-mini", Name: "GPT-5.1 Codex Mini"},
|
||||
{ID: "gpt-5.1", Name: "GPT-5.1"},
|
||||
})
|
||||
}
|
||||
|
||||
func withReasoningCapabilities(models []ModelInfo) []ModelInfo {
|
||||
result := make([]ModelInfo, 0, len(models))
|
||||
for _, model := range models {
|
||||
next := model
|
||||
next.Reasoning = providers.LookupReasoningCapability(model.ID)
|
||||
result = append(result, next)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
func TestProvidersHandlerListProviderModelsChatGPTOAuthIncludesReasoningMetadata(t *testing.T) {
|
||||
token := setupProvidersAdminToken(t)
|
||||
providerStore := newMockProviderStore()
|
||||
provider := &store.LLMProviderData{
|
||||
BaseModel: store.BaseModel{ID: uuid.New()},
|
||||
Name: "openai-codex",
|
||||
ProviderType: store.ProviderChatGPTOAuth,
|
||||
Enabled: true,
|
||||
Settings: json.RawMessage(`{
|
||||
"reasoning_defaults": {"effort": "high"}
|
||||
}`),
|
||||
}
|
||||
if err := providerStore.CreateProvider(t.Context(), provider); err != nil {
|
||||
t.Fatalf("CreateProvider() error = %v", err)
|
||||
}
|
||||
|
||||
handler := NewProvidersHandler(providerStore, newMockSecretsStore(), nil, "")
|
||||
mux := http.NewServeMux()
|
||||
handler.RegisterRoutes(mux)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/providers/"+provider.ID.String()+"/models", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w := httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status code = %d, want %d, body=%s", w.Code, http.StatusOK, w.Body.String())
|
||||
}
|
||||
|
||||
var result ProviderModelsResponse
|
||||
if err := json.NewDecoder(w.Body).Decode(&result); err != nil {
|
||||
t.Fatalf("Decode() error = %v", err)
|
||||
}
|
||||
if len(result.Models) == 0 {
|
||||
t.Fatal("models = empty, want hardcoded ChatGPT OAuth list")
|
||||
}
|
||||
if result.ReasoningDefaults == nil {
|
||||
t.Fatal("reasoning_defaults = nil, want provider defaults")
|
||||
}
|
||||
if result.ReasoningDefaults.Effort != "high" {
|
||||
t.Fatalf("reasoning_defaults.effort = %q, want high", result.ReasoningDefaults.Effort)
|
||||
}
|
||||
|
||||
var found bool
|
||||
for _, model := range result.Models {
|
||||
if model.ID != "gpt-5.4" {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if model.Reasoning == nil {
|
||||
t.Fatal("gpt-5.4 reasoning = nil, want capability metadata")
|
||||
}
|
||||
if model.Reasoning.DefaultEffort != "none" {
|
||||
t.Fatalf("gpt-5.4 default_effort = %q, want none", model.Reasoning.DefaultEffort)
|
||||
}
|
||||
if got := model.Reasoning.Levels; len(got) != 5 || got[4] != "xhigh" {
|
||||
t.Fatalf("gpt-5.4 levels = %#v, want none..xhigh", got)
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("gpt-5.4 not found in ChatGPT OAuth model list")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvidersHandlerListProviderModelsOpenAICompatAnnotatesKnownModels(t *testing.T) {
|
||||
token := setupProvidersAdminToken(t)
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/models" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"data": []map[string]string{
|
||||
{"id": "gpt-5.1-codex-max"},
|
||||
{"id": "gpt-5.4-experimental"},
|
||||
},
|
||||
})
|
||||
}))
|
||||
t.Cleanup(upstream.Close)
|
||||
|
||||
providerStore := newMockProviderStore()
|
||||
provider := &store.LLMProviderData{
|
||||
BaseModel: store.BaseModel{ID: uuid.New()},
|
||||
Name: "openai",
|
||||
ProviderType: store.ProviderOpenAICompat,
|
||||
APIBase: upstream.URL,
|
||||
APIKey: "token",
|
||||
Enabled: true,
|
||||
}
|
||||
if err := providerStore.CreateProvider(t.Context(), provider); err != nil {
|
||||
t.Fatalf("CreateProvider() error = %v", err)
|
||||
}
|
||||
|
||||
handler := NewProvidersHandler(providerStore, newMockSecretsStore(), nil, "")
|
||||
mux := http.NewServeMux()
|
||||
handler.RegisterRoutes(mux)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/providers/"+provider.ID.String()+"/models", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w := httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status code = %d, want %d, body=%s", w.Code, http.StatusOK, w.Body.String())
|
||||
}
|
||||
|
||||
var result ProviderModelsResponse
|
||||
if err := json.NewDecoder(w.Body).Decode(&result); err != nil {
|
||||
t.Fatalf("Decode() error = %v", err)
|
||||
}
|
||||
if len(result.Models) != 2 {
|
||||
t.Fatalf("models len = %d, want 2", len(result.Models))
|
||||
}
|
||||
if result.Models[0].Reasoning == nil {
|
||||
t.Fatal("known GPT-5.1 Codex Max reasoning = nil, want capability metadata")
|
||||
}
|
||||
if result.Models[0].Reasoning.DefaultEffort != "none" {
|
||||
t.Fatalf("known model default_effort = %q, want none", result.Models[0].Reasoning.DefaultEffort)
|
||||
}
|
||||
if result.Models[1].Reasoning != nil {
|
||||
t.Fatalf("unknown GPT-5 variant reasoning = %#v, want nil", result.Models[1].Reasoning)
|
||||
}
|
||||
if result.ReasoningDefaults != nil {
|
||||
t.Fatalf("reasoning_defaults = %#v, want nil when provider has no saved defaults", result.ReasoningDefaults)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package providers
|
||||
|
||||
import "strings"
|
||||
|
||||
// ReasoningCapability describes the supported reasoning levels for a model.
|
||||
// The registry is intentionally narrow: only GPT-5/Codex families we can
|
||||
// validate reliably today get explicit entries.
|
||||
type ReasoningCapability struct {
|
||||
Levels []string `json:"levels,omitempty"`
|
||||
DefaultEffort string `json:"default_effort,omitempty"`
|
||||
}
|
||||
|
||||
func (c *ReasoningCapability) Supports(level string) bool {
|
||||
if c == nil || level == "" {
|
||||
return false
|
||||
}
|
||||
for _, supported := range c.Levels {
|
||||
if supported == level {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type reasoningCapabilityEntry struct {
|
||||
id string
|
||||
capability ReasoningCapability
|
||||
}
|
||||
|
||||
var reasoningCapabilityEntries = []reasoningCapabilityEntry{
|
||||
{id: "gpt-5.4-mini", capability: ReasoningCapability{Levels: []string{"none", "low", "medium", "high", "xhigh"}, DefaultEffort: "none"}},
|
||||
{id: "gpt-5-mini", capability: ReasoningCapability{Levels: []string{"none", "low", "medium", "high", "xhigh"}, DefaultEffort: "none"}},
|
||||
{id: "gpt-5.4", capability: ReasoningCapability{Levels: []string{"none", "low", "medium", "high", "xhigh"}, DefaultEffort: "none"}},
|
||||
{id: "gpt-5.3-codex-spark", capability: ReasoningCapability{Levels: []string{"low", "medium", "high", "xhigh"}, DefaultEffort: "medium"}},
|
||||
{id: "gpt-5.3-codex", capability: ReasoningCapability{Levels: []string{"low", "medium", "high", "xhigh"}, DefaultEffort: "medium"}},
|
||||
{id: "gpt-5.2-codex", capability: ReasoningCapability{Levels: []string{"low", "medium", "high", "xhigh"}, DefaultEffort: "medium"}},
|
||||
{id: "gpt-5.2", capability: ReasoningCapability{Levels: []string{"none", "low", "medium", "high", "xhigh"}, DefaultEffort: "none"}},
|
||||
{id: "gpt-5.1-codex-max", capability: ReasoningCapability{Levels: []string{"none", "medium", "high", "xhigh"}, DefaultEffort: "none"}},
|
||||
{id: "gpt-5.1-codex-mini", capability: ReasoningCapability{Levels: []string{"low", "medium", "high"}, DefaultEffort: "medium"}},
|
||||
{id: "gpt-5.1-codex", capability: ReasoningCapability{Levels: []string{"low", "medium", "high"}, DefaultEffort: "medium"}},
|
||||
{id: "gpt-5.1", capability: ReasoningCapability{Levels: []string{"none", "low", "medium", "high"}, DefaultEffort: "none"}},
|
||||
{id: "gpt-5-codex-mini", capability: ReasoningCapability{Levels: []string{"low", "medium", "high"}, DefaultEffort: "medium"}},
|
||||
{id: "gpt-5-codex", capability: ReasoningCapability{Levels: []string{"low", "medium", "high"}, DefaultEffort: "medium"}},
|
||||
{id: "gpt-5", capability: ReasoningCapability{Levels: []string{"minimal", "low", "medium", "high"}, DefaultEffort: "medium"}},
|
||||
}
|
||||
|
||||
func LookupReasoningCapability(model string) *ReasoningCapability {
|
||||
normalized := normalizeReasoningModel(model)
|
||||
if normalized == "" {
|
||||
return nil
|
||||
}
|
||||
for _, entry := range reasoningCapabilityEntries {
|
||||
if normalized == entry.id {
|
||||
capability := entry.capability
|
||||
capability.Levels = append([]string(nil), capability.Levels...)
|
||||
return &capability
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeReasoningModel(model string) string {
|
||||
normalized := strings.ToLower(strings.TrimSpace(model))
|
||||
if normalized == "" {
|
||||
return ""
|
||||
}
|
||||
if idx := strings.LastIndex(normalized, "/"); idx >= 0 {
|
||||
normalized = normalized[idx+1:]
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
const ReasoningMetadataKey = "reasoning"
|
||||
|
||||
type reasoningDecisionKey struct{}
|
||||
|
||||
func WithReasoningDecision(ctx context.Context, decision ReasoningDecision) context.Context {
|
||||
return context.WithValue(ctx, reasoningDecisionKey{}, &decision)
|
||||
}
|
||||
|
||||
func ReasoningDecisionFromContext(ctx context.Context) *ReasoningDecision {
|
||||
decision, _ := ctx.Value(reasoningDecisionKey{}).(*ReasoningDecision)
|
||||
return decision
|
||||
}
|
||||
|
||||
func MergeReasoningMetadata(existing json.RawMessage, decision ReasoningDecision) json.RawMessage {
|
||||
if !decision.HasObservation() {
|
||||
return existing
|
||||
}
|
||||
payload := map[string]any{}
|
||||
if len(existing) > 0 {
|
||||
_ = json.Unmarshal(existing, &payload)
|
||||
}
|
||||
payload[ReasoningMetadataKey] = decision
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return existing
|
||||
}
|
||||
return json.RawMessage(data)
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package providers
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
ReasoningFallbackDowngrade = "downgrade"
|
||||
ReasoningFallbackDisable = "off"
|
||||
ReasoningFallbackProviderDefault = "provider_default"
|
||||
)
|
||||
|
||||
type ReasoningDecision struct {
|
||||
Source string `json:"source,omitempty"`
|
||||
RequestedEffort string `json:"requested_effort,omitempty"`
|
||||
EffectiveEffort string `json:"effective_effort,omitempty"`
|
||||
Fallback string `json:"fallback,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
KnownModel bool `json:"known_model,omitempty"`
|
||||
SupportedLevels []string `json:"supported_levels,omitempty"`
|
||||
UsedProviderDefault bool `json:"used_provider_default,omitempty"`
|
||||
}
|
||||
|
||||
func ResolveReasoningDecision(provider Provider, model, requestedEffort, fallback, source string) ReasoningDecision {
|
||||
decision := ReasoningDecision{
|
||||
Source: normalizeReasoningSource(source),
|
||||
RequestedEffort: NormalizeReasoningEffort(requestedEffort),
|
||||
Fallback: NormalizeReasoningFallback(fallback),
|
||||
}
|
||||
if decision.RequestedEffort == "" {
|
||||
decision.RequestedEffort = "off"
|
||||
}
|
||||
if decision.RequestedEffort == "off" {
|
||||
decision.EffectiveEffort = "off"
|
||||
return decision
|
||||
}
|
||||
tc, ok := provider.(ThinkingCapable)
|
||||
if !ok || !tc.SupportsThinking() {
|
||||
decision.EffectiveEffort = "off"
|
||||
decision.Reason = "provider does not support reasoning controls"
|
||||
return decision
|
||||
}
|
||||
capability := LookupReasoningCapability(model)
|
||||
if capability == nil {
|
||||
if decision.RequestedEffort == "auto" {
|
||||
decision.UsedProviderDefault = true
|
||||
decision.Reason = "unknown model; leaving provider default reasoning"
|
||||
return decision
|
||||
}
|
||||
decision.EffectiveEffort = decision.RequestedEffort
|
||||
decision.Reason = "unknown model; passing requested reasoning effort through"
|
||||
return decision
|
||||
}
|
||||
|
||||
decision.KnownModel = true
|
||||
decision.SupportedLevels = append([]string(nil), capability.Levels...)
|
||||
if decision.RequestedEffort == "auto" {
|
||||
decision.EffectiveEffort = capability.DefaultEffort
|
||||
decision.UsedProviderDefault = true
|
||||
decision.Reason = "auto uses the model default reasoning effort"
|
||||
return decision
|
||||
}
|
||||
if capability.Supports(decision.RequestedEffort) {
|
||||
decision.EffectiveEffort = decision.RequestedEffort
|
||||
return decision
|
||||
}
|
||||
|
||||
switch decision.Fallback {
|
||||
case ReasoningFallbackDisable:
|
||||
decision.EffectiveEffort = "off"
|
||||
decision.Reason = "requested reasoning effort is unsupported; disabled by fallback policy"
|
||||
case ReasoningFallbackProviderDefault:
|
||||
decision.EffectiveEffort = capability.DefaultEffort
|
||||
decision.UsedProviderDefault = true
|
||||
decision.Reason = "requested reasoning effort is unsupported; using model default"
|
||||
default:
|
||||
decision.EffectiveEffort = downgradeReasoningLevel(decision.RequestedEffort, capability.Levels)
|
||||
if decision.EffectiveEffort == "" {
|
||||
decision.EffectiveEffort = "off"
|
||||
decision.Reason = "requested reasoning effort is unsupported; disabled because no lower supported level exists"
|
||||
return decision
|
||||
}
|
||||
decision.Reason = "requested reasoning effort is unsupported; downgraded to the highest supported level not exceeding the request"
|
||||
}
|
||||
return decision
|
||||
}
|
||||
|
||||
func (d ReasoningDecision) RequestEffort() string {
|
||||
if d.UsedProviderDefault || d.EffectiveEffort == "" || d.EffectiveEffort == "off" {
|
||||
return ""
|
||||
}
|
||||
return d.EffectiveEffort
|
||||
}
|
||||
|
||||
func (d ReasoningDecision) HasObservation() bool {
|
||||
return d.Source != "" && d.Source != "unset"
|
||||
}
|
||||
|
||||
// NormalizeReasoningEffort returns the canonical lowercase effort level if valid, else "".
|
||||
func NormalizeReasoningEffort(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "off", "auto", "none", "minimal", "low", "medium", "high", "xhigh":
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeReasoningFallback returns the canonical fallback policy; defaults to "downgrade".
|
||||
func NormalizeReasoningFallback(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case ReasoningFallbackDisable, ReasoningFallbackProviderDefault:
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
default:
|
||||
return ReasoningFallbackDowngrade
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeReasoningSource(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "reasoning", "thinking_level", "provider_default":
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
default:
|
||||
return "unset"
|
||||
}
|
||||
}
|
||||
|
||||
func downgradeReasoningLevel(requested string, supported []string) string {
|
||||
ordered := reasoningOrder()
|
||||
requestRank, ok := ordered[requested]
|
||||
if !ok || len(supported) == 0 {
|
||||
return ""
|
||||
}
|
||||
bestLevel := ""
|
||||
bestRank := -1
|
||||
for _, level := range supported {
|
||||
rank, ok := ordered[level]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if rank <= requestRank && rank > bestRank {
|
||||
bestLevel = level
|
||||
bestRank = rank
|
||||
}
|
||||
}
|
||||
return bestLevel
|
||||
}
|
||||
|
||||
func reasoningOrder() map[string]int {
|
||||
return map[string]int{
|
||||
"none": 0, "minimal": 1, "low": 2, "medium": 3, "high": 4, "xhigh": 5,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testThinkingProvider struct {
|
||||
thinking bool
|
||||
}
|
||||
|
||||
func (p testThinkingProvider) Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (p testThinkingProvider) ChatStream(ctx context.Context, req ChatRequest, onChunk func(StreamChunk)) (*ChatResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (p testThinkingProvider) DefaultModel() string { return "gpt-5.4" }
|
||||
func (p testThinkingProvider) Name() string { return "test" }
|
||||
func (p testThinkingProvider) SupportsThinking() bool { return p.thinking }
|
||||
|
||||
func TestLookupReasoningCapability(t *testing.T) {
|
||||
capability := LookupReasoningCapability("openai/gpt-5.1-codex-max")
|
||||
if capability == nil {
|
||||
t.Fatal("LookupReasoningCapability() = nil, want capability")
|
||||
}
|
||||
if !capability.Supports("xhigh") {
|
||||
t.Fatal("expected gpt-5.1-codex-max to support xhigh")
|
||||
}
|
||||
if capability.Supports("low") {
|
||||
t.Fatal("expected gpt-5.1-codex-max to reject low")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveReasoningDecisionDowngradesUnsupportedEffort(t *testing.T) {
|
||||
decision := ResolveReasoningDecision(testThinkingProvider{thinking: true}, "gpt-5.1-codex", "xhigh", "downgrade", "reasoning")
|
||||
if decision.EffectiveEffort != "high" {
|
||||
t.Fatalf("EffectiveEffort = %q, want high", decision.EffectiveEffort)
|
||||
}
|
||||
if decision.RequestEffort() != "high" {
|
||||
t.Fatalf("RequestEffort() = %q, want high", decision.RequestEffort())
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveReasoningDecisionDowngradeNeverRaisesEffort(t *testing.T) {
|
||||
decision := ResolveReasoningDecision(testThinkingProvider{thinking: true}, "gpt-5.1-codex-max", "low", "downgrade", "reasoning")
|
||||
if decision.EffectiveEffort != "none" {
|
||||
t.Fatalf("EffectiveEffort = %q, want none", decision.EffectiveEffort)
|
||||
}
|
||||
if decision.RequestEffort() != "none" {
|
||||
t.Fatalf("RequestEffort() = %q, want none", decision.RequestEffort())
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveReasoningDecisionDowngradeDisablesWhenNoLowerLevelExists(t *testing.T) {
|
||||
decision := ResolveReasoningDecision(testThinkingProvider{thinking: true}, "gpt-5.1-codex", "minimal", "downgrade", "reasoning")
|
||||
if decision.EffectiveEffort != "off" {
|
||||
t.Fatalf("EffectiveEffort = %q, want off", decision.EffectiveEffort)
|
||||
}
|
||||
if decision.RequestEffort() != "" {
|
||||
t.Fatalf("RequestEffort() = %q, want empty", decision.RequestEffort())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupReasoningCapabilityRejectsUnknownSuffixVariants(t *testing.T) {
|
||||
if capability := LookupReasoningCapability("gpt-5.4-experimental"); capability != nil {
|
||||
t.Fatalf("LookupReasoningCapability() = %#v, want nil", capability)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveReasoningDecisionUsesProviderDefaultForAuto(t *testing.T) {
|
||||
decision := ResolveReasoningDecision(testThinkingProvider{thinking: true}, "gpt-5.4", "auto", "provider_default", "reasoning")
|
||||
if !decision.UsedProviderDefault {
|
||||
t.Fatal("UsedProviderDefault = false, want true")
|
||||
}
|
||||
if decision.EffectiveEffort != "none" {
|
||||
t.Fatalf("EffectiveEffort = %q, want none", decision.EffectiveEffort)
|
||||
}
|
||||
if decision.RequestEffort() != "" {
|
||||
t.Fatalf("RequestEffort() = %q, want empty", decision.RequestEffort())
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveReasoningDecisionDisablesWhenProviderCannotReason(t *testing.T) {
|
||||
decision := ResolveReasoningDecision(testThinkingProvider{}, "gpt-5.4", "high", "downgrade", "thinking_level")
|
||||
if decision.EffectiveEffort != "off" {
|
||||
t.Fatalf("EffectiveEffort = %q, want off", decision.EffectiveEffort)
|
||||
}
|
||||
}
|
||||
+127
-10
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/config"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
)
|
||||
|
||||
// sanitizeToolCallPrefix strips characters not in [a-z0-9_{}] from the prefix.
|
||||
@@ -155,22 +156,71 @@ func (a *AgentData) ParseMemoryConfig() *config.MemoryConfig {
|
||||
return &c
|
||||
}
|
||||
|
||||
// ParseThinkingLevel extracts thinking_level from other_config JSONB.
|
||||
// Returns "low" if not configured (default thinking mode).
|
||||
// ParseThinkingLevel extracts the normalized reasoning effort from other_config JSONB.
|
||||
// Missing config defaults to "off" to match the dashboard and docs.
|
||||
func (a *AgentData) ParseThinkingLevel() string {
|
||||
return a.ParseReasoningConfig().Effort
|
||||
}
|
||||
|
||||
// ParseReasoningConfig extracts additive advanced reasoning settings from other_config.
|
||||
// Legacy thinking_level remains a backward-compatible fallback source.
|
||||
func (a *AgentData) ParseReasoningConfig() AgentReasoningConfig {
|
||||
cfg := AgentReasoningConfig{
|
||||
OverrideMode: ReasoningOverrideInherit,
|
||||
Effort: "off",
|
||||
Fallback: ReasoningFallbackDowngrade,
|
||||
Source: ReasoningSourceUnset,
|
||||
}
|
||||
if len(a.OtherConfig) == 0 {
|
||||
return "low"
|
||||
return cfg
|
||||
}
|
||||
var cfg struct {
|
||||
ThinkingLevel string `json:"thinking_level"`
|
||||
|
||||
var raw map[string]json.RawMessage
|
||||
if json.Unmarshal(a.OtherConfig, &raw) != nil {
|
||||
return cfg
|
||||
}
|
||||
if json.Unmarshal(a.OtherConfig, &cfg) != nil {
|
||||
return "low"
|
||||
|
||||
var reasoning struct {
|
||||
OverrideMode string `json:"override_mode"`
|
||||
Effort string `json:"effort"`
|
||||
Fallback string `json:"fallback"`
|
||||
}
|
||||
if cfg.ThinkingLevel == "" {
|
||||
return "low"
|
||||
explicitInherit := false
|
||||
if data, ok := raw["reasoning"]; ok && len(data) > 0 && json.Unmarshal(data, &reasoning) == nil {
|
||||
if reasoning.OverrideMode == ReasoningOverrideInherit {
|
||||
explicitInherit = true
|
||||
cfg.OverrideMode = ReasoningOverrideInherit
|
||||
cfg.Source = ReasoningSourceUnset
|
||||
cfg.Effort = "off"
|
||||
cfg.Fallback = ReasoningFallbackDowngrade
|
||||
} else {
|
||||
cfg.OverrideMode = ReasoningOverrideCustom
|
||||
cfg.Source = ReasoningSourceAdvanced
|
||||
if effort := normalizeReasoningEffort(reasoning.Effort); effort != "" {
|
||||
cfg.Effort = effort
|
||||
}
|
||||
cfg.Fallback = normalizeReasoningFallback(reasoning.Fallback)
|
||||
}
|
||||
}
|
||||
return cfg.ThinkingLevel
|
||||
|
||||
if !explicitInherit {
|
||||
if data, ok := raw["thinking_level"]; ok && len(data) > 0 {
|
||||
var legacy string
|
||||
if json.Unmarshal(data, &legacy) == nil {
|
||||
if effort := normalizeReasoningEffort(legacy); effort != "" {
|
||||
if cfg.Source == ReasoningSourceUnset {
|
||||
cfg.OverrideMode = ReasoningOverrideCustom
|
||||
cfg.Source = ReasoningSourceLegacy
|
||||
cfg.Effort = effort
|
||||
} else if cfg.Effort == "off" {
|
||||
cfg.Effort = effort
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
// ParseMaxTokens extracts max_tokens from other_config JSONB.
|
||||
@@ -238,6 +288,16 @@ func (a *AgentData) ParseSkillNudgeInterval() int {
|
||||
return *cfg.SkillNudgeInterval
|
||||
}
|
||||
|
||||
// normalizeReasoningEffort delegates to providers.NormalizeReasoningEffort (DRY).
|
||||
func normalizeReasoningEffort(value string) string {
|
||||
return providers.NormalizeReasoningEffort(value)
|
||||
}
|
||||
|
||||
// normalizeReasoningFallback delegates to providers.NormalizeReasoningFallback (DRY).
|
||||
func normalizeReasoningFallback(value string) string {
|
||||
return providers.NormalizeReasoningFallback(value)
|
||||
}
|
||||
|
||||
// WorkspaceSharingConfig controls per-user workspace isolation.
|
||||
// When shared_dm/shared_group is true, users share the base workspace directory
|
||||
// instead of each getting an isolated subfolder.
|
||||
@@ -249,6 +309,63 @@ type WorkspaceSharingConfig struct {
|
||||
ShareKnowledgeGraph bool `json:"share_knowledge_graph"`
|
||||
}
|
||||
|
||||
const (
|
||||
ReasoningSourceUnset = "unset"
|
||||
ReasoningSourceLegacy = "thinking_level"
|
||||
ReasoningSourceAdvanced = "reasoning"
|
||||
ReasoningSourceProviderDefault = "provider_default"
|
||||
// Reasoning fallback constants — canonical definitions in providers package.
|
||||
ReasoningFallbackDowngrade = providers.ReasoningFallbackDowngrade
|
||||
ReasoningFallbackDisable = providers.ReasoningFallbackDisable
|
||||
ReasoningFallbackProviderDefault = providers.ReasoningFallbackProviderDefault
|
||||
ReasoningOverrideInherit = "inherit"
|
||||
ReasoningOverrideCustom = "custom"
|
||||
)
|
||||
|
||||
type AgentReasoningConfig struct {
|
||||
OverrideMode string `json:"override_mode,omitempty"`
|
||||
Effort string `json:"effort,omitempty"`
|
||||
Fallback string `json:"fallback,omitempty"`
|
||||
Source string `json:"-"`
|
||||
}
|
||||
|
||||
// ResolveEffectiveReasoningConfig applies provider-owned defaults unless the agent
|
||||
// has an explicit custom reasoning override.
|
||||
func ResolveEffectiveReasoningConfig(
|
||||
providerDefaults *ProviderReasoningConfig,
|
||||
agentConfig AgentReasoningConfig,
|
||||
) AgentReasoningConfig {
|
||||
if agentConfig.OverrideMode == "" {
|
||||
agentConfig.OverrideMode = ReasoningOverrideInherit
|
||||
}
|
||||
if agentConfig.Fallback == "" {
|
||||
agentConfig.Fallback = ReasoningFallbackDowngrade
|
||||
}
|
||||
if agentConfig.Effort == "" {
|
||||
agentConfig.Effort = "off"
|
||||
}
|
||||
|
||||
if agentConfig.OverrideMode == ReasoningOverrideCustom {
|
||||
return agentConfig
|
||||
}
|
||||
|
||||
if providerDefaults == nil {
|
||||
return AgentReasoningConfig{
|
||||
OverrideMode: ReasoningOverrideInherit,
|
||||
Effort: "off",
|
||||
Fallback: ReasoningFallbackDowngrade,
|
||||
Source: ReasoningSourceUnset,
|
||||
}
|
||||
}
|
||||
|
||||
return AgentReasoningConfig{
|
||||
OverrideMode: ReasoningOverrideInherit,
|
||||
Effort: providerDefaults.Effort,
|
||||
Fallback: providerDefaults.Fallback,
|
||||
Source: ReasoningSourceProviderDefault,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
ChatGPTOAuthStrategyManual = "manual" // legacy alias
|
||||
ChatGPTOAuthStrategyPrimaryFirst = "primary_first"
|
||||
|
||||
@@ -6,6 +6,160 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseReasoningConfigDefaultsToOff(t *testing.T) {
|
||||
agent := &AgentData{}
|
||||
|
||||
got := agent.ParseReasoningConfig()
|
||||
if got.OverrideMode != ReasoningOverrideInherit {
|
||||
t.Fatalf("OverrideMode = %q, want %q", got.OverrideMode, ReasoningOverrideInherit)
|
||||
}
|
||||
if got.Effort != "off" {
|
||||
t.Fatalf("Effort = %q, want off", got.Effort)
|
||||
}
|
||||
if got.Fallback != ReasoningFallbackDowngrade {
|
||||
t.Fatalf("Fallback = %q, want %q", got.Fallback, ReasoningFallbackDowngrade)
|
||||
}
|
||||
if got.Source != ReasoningSourceUnset {
|
||||
t.Fatalf("Source = %q, want %q", got.Source, ReasoningSourceUnset)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseReasoningConfigUsesLegacyThinkingLevel(t *testing.T) {
|
||||
agent := &AgentData{
|
||||
OtherConfig: json.RawMessage(`{"thinking_level":"medium"}`),
|
||||
}
|
||||
|
||||
got := agent.ParseReasoningConfig()
|
||||
if got.Effort != "medium" {
|
||||
t.Fatalf("Effort = %q, want medium", got.Effort)
|
||||
}
|
||||
if got.OverrideMode != ReasoningOverrideCustom {
|
||||
t.Fatalf("OverrideMode = %q, want %q", got.OverrideMode, ReasoningOverrideCustom)
|
||||
}
|
||||
if got.Source != ReasoningSourceLegacy {
|
||||
t.Fatalf("Source = %q, want %q", got.Source, ReasoningSourceLegacy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseReasoningConfigPrefersAdvancedSettings(t *testing.T) {
|
||||
agent := &AgentData{
|
||||
OtherConfig: json.RawMessage(`{
|
||||
"thinking_level": "high",
|
||||
"reasoning": {"effort": "xhigh", "fallback": "provider_default"}
|
||||
}`),
|
||||
}
|
||||
|
||||
got := agent.ParseReasoningConfig()
|
||||
if got.Effort != "xhigh" {
|
||||
t.Fatalf("Effort = %q, want xhigh", got.Effort)
|
||||
}
|
||||
if got.OverrideMode != ReasoningOverrideCustom {
|
||||
t.Fatalf("OverrideMode = %q, want %q", got.OverrideMode, ReasoningOverrideCustom)
|
||||
}
|
||||
if got.Fallback != ReasoningFallbackProviderDefault {
|
||||
t.Fatalf("Fallback = %q, want %q", got.Fallback, ReasoningFallbackProviderDefault)
|
||||
}
|
||||
if got.Source != ReasoningSourceAdvanced {
|
||||
t.Fatalf("Source = %q, want %q", got.Source, ReasoningSourceAdvanced)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseReasoningConfigKeepsLegacyEffortWhenAdvancedOnlySetsFallback(t *testing.T) {
|
||||
agent := &AgentData{
|
||||
OtherConfig: json.RawMessage(`{
|
||||
"thinking_level": "medium",
|
||||
"reasoning": {"fallback": "off"}
|
||||
}`),
|
||||
}
|
||||
|
||||
got := agent.ParseReasoningConfig()
|
||||
if got.Effort != "medium" {
|
||||
t.Fatalf("Effort = %q, want medium", got.Effort)
|
||||
}
|
||||
if got.Fallback != ReasoningFallbackDisable {
|
||||
t.Fatalf("Fallback = %q, want %q", got.Fallback, ReasoningFallbackDisable)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseReasoningConfigPreservesExplicitInherit(t *testing.T) {
|
||||
agent := &AgentData{
|
||||
OtherConfig: json.RawMessage(`{
|
||||
"thinking_level": "high",
|
||||
"reasoning": {"override_mode": "inherit"}
|
||||
}`),
|
||||
}
|
||||
|
||||
got := agent.ParseReasoningConfig()
|
||||
if got.OverrideMode != ReasoningOverrideInherit {
|
||||
t.Fatalf("OverrideMode = %q, want %q", got.OverrideMode, ReasoningOverrideInherit)
|
||||
}
|
||||
if got.Effort != "off" {
|
||||
t.Fatalf("Effort = %q, want off", got.Effort)
|
||||
}
|
||||
if got.Source != ReasoningSourceUnset {
|
||||
t.Fatalf("Source = %q, want %q", got.Source, ReasoningSourceUnset)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseProviderReasoningConfigNormalizesDefaults(t *testing.T) {
|
||||
settings := json.RawMessage(`{
|
||||
"reasoning_defaults": {"effort": " xhigh ", "fallback": "provider_default"}
|
||||
}`)
|
||||
|
||||
got := ParseProviderReasoningConfig(settings)
|
||||
if got == nil {
|
||||
t.Fatal("ParseProviderReasoningConfig() = nil, want config")
|
||||
}
|
||||
if got.Effort != "xhigh" {
|
||||
t.Fatalf("Effort = %q, want xhigh", got.Effort)
|
||||
}
|
||||
if got.Fallback != ReasoningFallbackProviderDefault {
|
||||
t.Fatalf("Fallback = %q, want %q", got.Fallback, ReasoningFallbackProviderDefault)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveEffectiveReasoningConfigUsesProviderDefaults(t *testing.T) {
|
||||
got := ResolveEffectiveReasoningConfig(
|
||||
&ProviderReasoningConfig{Effort: "medium", Fallback: ReasoningFallbackDisable},
|
||||
AgentReasoningConfig{OverrideMode: ReasoningOverrideInherit},
|
||||
)
|
||||
|
||||
if got.OverrideMode != ReasoningOverrideInherit {
|
||||
t.Fatalf("OverrideMode = %q, want %q", got.OverrideMode, ReasoningOverrideInherit)
|
||||
}
|
||||
if got.Effort != "medium" {
|
||||
t.Fatalf("Effort = %q, want medium", got.Effort)
|
||||
}
|
||||
if got.Fallback != ReasoningFallbackDisable {
|
||||
t.Fatalf("Fallback = %q, want %q", got.Fallback, ReasoningFallbackDisable)
|
||||
}
|
||||
if got.Source != ReasoningSourceProviderDefault {
|
||||
t.Fatalf("Source = %q, want %q", got.Source, ReasoningSourceProviderDefault)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveEffectiveReasoningConfigPreservesCustomAgentReasoning(t *testing.T) {
|
||||
got := ResolveEffectiveReasoningConfig(
|
||||
&ProviderReasoningConfig{Effort: "medium", Fallback: ReasoningFallbackDisable},
|
||||
AgentReasoningConfig{
|
||||
OverrideMode: ReasoningOverrideCustom,
|
||||
Effort: "xhigh",
|
||||
Fallback: ReasoningFallbackProviderDefault,
|
||||
Source: ReasoningSourceAdvanced,
|
||||
},
|
||||
)
|
||||
|
||||
if got.OverrideMode != ReasoningOverrideCustom {
|
||||
t.Fatalf("OverrideMode = %q, want %q", got.OverrideMode, ReasoningOverrideCustom)
|
||||
}
|
||||
if got.Effort != "xhigh" {
|
||||
t.Fatalf("Effort = %q, want xhigh", got.Effort)
|
||||
}
|
||||
if got.Source != ReasoningSourceAdvanced {
|
||||
t.Fatalf("Source = %q, want %q", got.Source, ReasoningSourceAdvanced)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseChatGPTOAuthRoutingNormalizesNames(t *testing.T) {
|
||||
agent := &AgentData{
|
||||
OtherConfig: json.RawMessage(`{
|
||||
|
||||
@@ -86,10 +86,17 @@ const RequiredMemoryEmbeddingDimensions = 1536
|
||||
type EmbeddingSettings struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Model string `json:"model,omitempty"` // e.g. "text-embedding-3-small"
|
||||
APIBase string `json:"api_base,omitempty"` // override if embedding endpoint differs from chat
|
||||
APIBase string `json:"api_base,omitempty"` // override if embedding endpoint differs from chat
|
||||
Dimensions int `json:"dimensions,omitempty"` // truncate output to N dims (e.g. 1536); 0 = model default
|
||||
}
|
||||
|
||||
// ProviderReasoningConfig holds provider-owned default reasoning settings.
|
||||
// These defaults are inherited by agents unless they save a custom override.
|
||||
type ProviderReasoningConfig struct {
|
||||
Effort string `json:"effort,omitempty"`
|
||||
Fallback string `json:"fallback,omitempty"`
|
||||
}
|
||||
|
||||
// ChatGPTOAuthProviderSettings holds provider-level defaults for Codex account pooling.
|
||||
type ChatGPTOAuthProviderSettings struct {
|
||||
CodexPool *ChatGPTOAuthRoutingConfig `json:"codex_pool,omitempty"`
|
||||
@@ -127,6 +134,38 @@ func ParseChatGPTOAuthProviderSettings(settings json.RawMessage) *ChatGPTOAuthPr
|
||||
return &s
|
||||
}
|
||||
|
||||
// ParseProviderReasoningConfig extracts provider-owned reasoning defaults from settings JSONB.
|
||||
// Returns nil when no non-default provider reasoning is configured.
|
||||
func ParseProviderReasoningConfig(settings json.RawMessage) *ProviderReasoningConfig {
|
||||
if len(settings) == 0 {
|
||||
return nil
|
||||
}
|
||||
var raw struct {
|
||||
ReasoningDefaults *ProviderReasoningConfig `json:"reasoning_defaults"`
|
||||
}
|
||||
if json.Unmarshal(settings, &raw) != nil {
|
||||
return nil
|
||||
}
|
||||
return normalizeProviderReasoningConfig(raw.ReasoningDefaults)
|
||||
}
|
||||
|
||||
func normalizeProviderReasoningConfig(raw *ProviderReasoningConfig) *ProviderReasoningConfig {
|
||||
if raw == nil {
|
||||
return nil
|
||||
}
|
||||
cfg := &ProviderReasoningConfig{
|
||||
Effort: normalizeReasoningEffort(raw.Effort),
|
||||
Fallback: normalizeReasoningFallback(raw.Fallback),
|
||||
}
|
||||
if cfg.Effort == "" {
|
||||
cfg.Effort = "off"
|
||||
}
|
||||
if cfg.Effort == "off" && cfg.Fallback == ReasoningFallbackDowngrade {
|
||||
return nil
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// NoEmbeddingTypes lists provider types that cannot serve embeddings.
|
||||
var NoEmbeddingTypes = map[string]bool{
|
||||
ProviderAnthropicNative: true, // uses x-api-key auth, not Bearer; no embedding models
|
||||
|
||||
@@ -97,7 +97,7 @@ export function ProviderModelSelect({
|
||||
[enabledProviders, provider],
|
||||
);
|
||||
const selectedProviderId = selectedProvider?.id;
|
||||
const { models, loading: modelsLoading } = useProviderModels(selectedProviderId, selectedProvider?.provider_type);
|
||||
const { models, loading: modelsLoading } = useProviderModels(selectedProviderId);
|
||||
const { verify, verifying, result: verifyResult, reset: resetVerify } = useProviderVerify();
|
||||
|
||||
const hasSavedValues = savedProvider !== undefined && savedModel !== undefined;
|
||||
|
||||
@@ -647,15 +647,48 @@
|
||||
"thinking": {
|
||||
"title": "Extended Thinking",
|
||||
"description": "Allow the model to reason before responding. Higher levels use more tokens but produce better results on complex tasks.",
|
||||
"modeLabel": "Reasoning Mode",
|
||||
"inherit": "Use Provider Defaults",
|
||||
"custom": "Custom For This Agent",
|
||||
"customDesc": "Override the provider-owned default reasoning policy for this agent only.",
|
||||
"providerDefaultSummary": "Inherited from {{provider}}",
|
||||
"providerLabelFallback": "provider defaults",
|
||||
"noProviderDefault": "No provider-owned reasoning default is saved yet. Inherit mode will keep this agent on reasoning off until the provider saves one.",
|
||||
"thinkingLevel": "Thinking Level",
|
||||
"thinkingLevelTip": "Simple preset for the common case. Use Expert Mode when you need model-specific effort controls or explicit fallback behavior.",
|
||||
"expertMode": "Expert Mode",
|
||||
"expertModeDesc": "Request model-specific reasoning levels such as none, minimal, auto, or xhigh, and control what happens when the selected model does not support them.",
|
||||
"supportedLevelsForModel": "Selected model {{model}} supports these explicit reasoning levels.",
|
||||
"loadingSupport": "Loading reasoning capability metadata for {{model}}.",
|
||||
"unknownSupport": "GoClaw does not have explicit reasoning metadata for {{model}} yet.",
|
||||
"expertModeUnavailable": "Advanced reasoning controls are only available when the selected model has explicit capability metadata from the provider models endpoint.",
|
||||
"modelDefault": "Model default reasoning: {{level}}",
|
||||
"requestedEffort": "Requested Effort",
|
||||
"requestedEffortTip": "Requested reasoning level before GoClaw normalizes it against the selected model.",
|
||||
"fallbackBehavior": "Fallback Behavior",
|
||||
"fallbackBehaviorTip": "What GoClaw should do when the requested effort is not supported by the selected model.",
|
||||
"supportedOption": "supported for this model",
|
||||
"legacyShim": "GoClaw also saves a coarse thinking_level shim for backward compatibility with older builds.",
|
||||
"off": "Off",
|
||||
"offDesc": "No extended thinking",
|
||||
"auto": "Auto",
|
||||
"autoDesc": "Use the model default reasoning effort",
|
||||
"none": "None",
|
||||
"noneDesc": "Explicitly disable provider reasoning while keeping the advanced contract",
|
||||
"minimal": "Minimal",
|
||||
"minimalDesc": "Use the smallest supported reasoning effort above none",
|
||||
"low": "Low",
|
||||
"lowDesc": "~4K token budget",
|
||||
"medium": "Medium",
|
||||
"mediumDesc": "~10-16K token budget",
|
||||
"high": "High",
|
||||
"highDesc": "~32K token budget"
|
||||
"highDesc": "~32K token budget",
|
||||
"xhigh": "XHigh",
|
||||
"xhighDesc": "Use the maximum supported reasoning effort",
|
||||
"downgrade": "Downgrade",
|
||||
"downgradeDesc": "Normalize to the nearest supported lower level, or the lowest supported level if needed",
|
||||
"provider_default": "Use Model Default",
|
||||
"provider_defaultDesc": "Do not force an explicit effort and let the provider/model pick its default"
|
||||
},
|
||||
"otherConfig": {
|
||||
"title": "Other Config",
|
||||
|
||||
@@ -220,6 +220,17 @@
|
||||
"oauthProviderDefaultHint": "Use the pool defaults below to define the reusable OpenAI Codex routing policy for every agent that points at this alias.",
|
||||
"oauthRoutingHint": "Agent pages now show the effective pool for that agent, plus whether it inherits these defaults or saves a custom override.",
|
||||
"oauthManagedByHint": "This alias is managed by provider {{provider}}. Edit pool membership there instead of on this account.",
|
||||
"reasoningDefaultsTitle": "Reasoning Defaults",
|
||||
"reasoningDefaultsDescription": "Set the default reasoning policy for agents using this provider. Final runtime effort is still normalized against each agent's selected model.",
|
||||
"reasoningPreset": "Default Preset",
|
||||
"reasoningExpertMode": "Expert Defaults",
|
||||
"reasoningExpertModeDesc": "Use provider-owned advanced reasoning defaults when you need efforts such as auto, none, minimal, or xhigh.",
|
||||
"reasoningPreviewDescription": "Previewing explicit reasoning capability metadata for {{model}}.",
|
||||
"reasoningPreviewLabel": "Capability Preview",
|
||||
"reasoningPreviewDefault": "Preview model default reasoning: {{level}}",
|
||||
"reasoningPreviewEmpty": "No explicit reasoning-capable models were returned for this provider yet.",
|
||||
"reasoningRequestedEffort": "Requested Effort",
|
||||
"reasoningFallbackBehavior": "Fallback Behavior",
|
||||
"codexPoolDefaultsTitle": "Pool Defaults",
|
||||
"codexPoolDefaultsDescription": "Define the reusable OpenAI Codex pool owned by this alias for agents that use it as their primary provider.",
|
||||
"oauthDisplayNameRecommendation": "Add a display name if you want clearer labels when multiple OpenAI Codex OAuth accounts appear in the pool.",
|
||||
@@ -246,10 +257,33 @@
|
||||
"verifyFailed": "Embedding verification failed",
|
||||
"dimensionsMismatch": "{{count}} dimensions — does not match required 1536"
|
||||
},
|
||||
"reasoning": {
|
||||
"off": "Off",
|
||||
"offDesc": "No default extended thinking",
|
||||
"auto": "Auto",
|
||||
"autoDesc": "Ask the model to use its own default reasoning effort",
|
||||
"none": "None",
|
||||
"noneDesc": "Explicitly disable provider reasoning while keeping the advanced contract",
|
||||
"minimal": "Minimal",
|
||||
"minimalDesc": "Use the smallest supported reasoning effort above none",
|
||||
"low": "Low",
|
||||
"lowDesc": "~4K token budget",
|
||||
"medium": "Medium",
|
||||
"mediumDesc": "~10-16K token budget",
|
||||
"high": "High",
|
||||
"highDesc": "~32K token budget",
|
||||
"xhigh": "XHigh",
|
||||
"xhighDesc": "Use the maximum supported reasoning effort",
|
||||
"downgrade": "Downgrade",
|
||||
"downgradeDesc": "Normalize to the nearest supported lower level, or disable when no lower supported level exists",
|
||||
"provider_default": "Use Model Default",
|
||||
"provider_defaultDesc": "Let the selected model fall back to its own default reasoning effort"
|
||||
},
|
||||
"list": {
|
||||
"poolOwner": "Pool owner",
|
||||
"poolMember": "Pool member",
|
||||
"standalone": "Standalone",
|
||||
"reasoningDefault": "Reasoning {{level}}",
|
||||
"managedBy": "Managed by {{provider}}",
|
||||
"memberCount_one": "{{count}} member",
|
||||
"memberCount_other": "{{count}} members",
|
||||
|
||||
@@ -43,6 +43,19 @@
|
||||
"span": {
|
||||
"model": "Model:",
|
||||
"tokens": "Tokens:",
|
||||
"reasoning": "Reasoning:",
|
||||
"source": "source",
|
||||
"requested": "requested",
|
||||
"effective": "effective",
|
||||
"fallback": "fallback",
|
||||
"modelDefault": "model default",
|
||||
"supportedLevels": "supported:",
|
||||
"sourceValue": {
|
||||
"provider_default": "provider default",
|
||||
"reasoning": "agent advanced",
|
||||
"thinking_level": "legacy thinking level",
|
||||
"unset": "unset"
|
||||
},
|
||||
"createdAt": "Created:",
|
||||
"input": "Input:",
|
||||
"output": "Output:",
|
||||
|
||||
@@ -647,15 +647,48 @@
|
||||
"thinking": {
|
||||
"title": "Suy nghĩ mở rộng",
|
||||
"description": "Cho phép model suy luận trước khi phản hồi. Cấp độ cao hơn dùng nhiều token hơn nhưng cho kết quả tốt hơn với tác vụ phức tạp.",
|
||||
"modeLabel": "Reasoning Mode",
|
||||
"inherit": "Use Provider Defaults",
|
||||
"custom": "Custom For This Agent",
|
||||
"customDesc": "Override the provider-owned default reasoning policy for this agent only.",
|
||||
"providerDefaultSummary": "Inherited from {{provider}}",
|
||||
"providerLabelFallback": "provider defaults",
|
||||
"noProviderDefault": "No provider-owned reasoning default is saved yet. Inherit mode will keep this agent on reasoning off until the provider saves one.",
|
||||
"thinkingLevel": "Cấp độ suy nghĩ",
|
||||
"thinkingLevelTip": "Preset đơn giản cho phần lớn trường hợp. Bật Chế độ chuyên gia khi bạn cần mức reasoning theo model hoặc hành vi fallback rõ ràng.",
|
||||
"expertMode": "Chế độ chuyên gia",
|
||||
"expertModeDesc": "Yêu cầu các mức reasoning theo model như none, minimal, auto hoặc xhigh và kiểm soát cách xử lý khi model đã chọn không hỗ trợ.",
|
||||
"supportedLevelsForModel": "Model {{model}} hiện hỗ trợ các mức reasoning tường minh này.",
|
||||
"loadingSupport": "Đang tải metadata khả năng reasoning cho {{model}}.",
|
||||
"unknownSupport": "GoClaw chưa có metadata reasoning tường minh cho {{model}}.",
|
||||
"expertModeUnavailable": "Các điều khiển reasoning nâng cao chỉ khả dụng khi model đã chọn có metadata khả năng rõ ràng từ endpoint models của provider.",
|
||||
"modelDefault": "Reasoning mặc định của model: {{level}}",
|
||||
"requestedEffort": "Mức yêu cầu",
|
||||
"requestedEffortTip": "Mức reasoning được yêu cầu trước khi GoClaw chuẩn hóa theo model đã chọn.",
|
||||
"fallbackBehavior": "Hành vi fallback",
|
||||
"fallbackBehaviorTip": "GoClaw nên làm gì khi mức được yêu cầu không được model đã chọn hỗ trợ.",
|
||||
"supportedOption": "được model này hỗ trợ",
|
||||
"legacyShim": "GoClaw cũng lưu thinking_level dạng coarse để tương thích ngược với các build cũ.",
|
||||
"off": "Tắt",
|
||||
"offDesc": "Không có suy nghĩ mở rộng",
|
||||
"auto": "Tự động",
|
||||
"autoDesc": "Dùng mức reasoning mặc định của model",
|
||||
"none": "Không",
|
||||
"noneDesc": "Tắt reasoning của provider một cách tường minh nhưng vẫn giữ contract nâng cao",
|
||||
"minimal": "Tối thiểu",
|
||||
"minimalDesc": "Dùng mức reasoning nhỏ nhất được hỗ trợ cao hơn none",
|
||||
"low": "Thấp",
|
||||
"lowDesc": "~4K ngân sách token",
|
||||
"medium": "Trung bình",
|
||||
"mediumDesc": "~10-16K ngân sách token",
|
||||
"high": "Cao",
|
||||
"highDesc": "~32K ngân sách token"
|
||||
"highDesc": "~32K ngân sách token",
|
||||
"xhigh": "Rất cao",
|
||||
"xhighDesc": "Dùng mức reasoning cao nhất được hỗ trợ",
|
||||
"downgrade": "Giảm cấp",
|
||||
"downgradeDesc": "Chuẩn hóa xuống mức thấp hơn gần nhất được hỗ trợ hoặc mức thấp nhất được hỗ trợ nếu cần",
|
||||
"provider_default": "Dùng mặc định model",
|
||||
"provider_defaultDesc": "Không ép mức cụ thể và để provider/model tự chọn mặc định"
|
||||
},
|
||||
"otherConfig": {
|
||||
"title": "Cấu hình khác",
|
||||
|
||||
@@ -224,6 +224,17 @@
|
||||
"oauthProviderDefaultHint": "Dùng phần thiết lập pool mặc định bên dưới để định nghĩa chính sách routing OpenAI Codex dùng lại cho mọi agent trỏ vào bí danh này.",
|
||||
"oauthRoutingHint": "Trang agent giờ sẽ hiển thị pool hiệu lực của agent đó, cùng việc nó đang kế thừa mặc định này hay đang dùng override riêng.",
|
||||
"oauthManagedByHint": "Bí danh này đang được quản lý bởi provider {{provider}}. Hãy chỉnh membership ở provider đó thay vì tại tài khoản này.",
|
||||
"reasoningDefaultsTitle": "Reasoning Defaults",
|
||||
"reasoningDefaultsDescription": "Set the default reasoning policy for agents using this provider. Final runtime effort is still normalized against each agent's selected model.",
|
||||
"reasoningPreset": "Default Preset",
|
||||
"reasoningExpertMode": "Expert Defaults",
|
||||
"reasoningExpertModeDesc": "Use provider-owned advanced reasoning defaults when you need efforts such as auto, none, minimal, or xhigh.",
|
||||
"reasoningPreviewDescription": "Previewing explicit reasoning capability metadata for {{model}}.",
|
||||
"reasoningPreviewLabel": "Capability Preview",
|
||||
"reasoningPreviewDefault": "Preview model default reasoning: {{level}}",
|
||||
"reasoningPreviewEmpty": "No explicit reasoning-capable models were returned for this provider yet.",
|
||||
"reasoningRequestedEffort": "Requested Effort",
|
||||
"reasoningFallbackBehavior": "Fallback Behavior",
|
||||
"codexPoolDefaultsTitle": "Thiết lập pool mặc định",
|
||||
"codexPoolDefaultsDescription": "Định nghĩa pool OpenAI Codex dùng lại do chính bí danh này sở hữu cho các agent dùng nó làm provider chính.",
|
||||
"oauthDisplayNameRecommendation": "Thêm tên hiển thị nếu bạn muốn nhãn rõ ràng hơn khi nhiều tài khoản OpenAI Codex OAuth xuất hiện trong pool.",
|
||||
@@ -240,6 +251,7 @@
|
||||
"poolOwner": "Chủ pool",
|
||||
"poolMember": "Thành viên pool",
|
||||
"standalone": "Độc lập",
|
||||
"reasoningDefault": "Reasoning {{level}}",
|
||||
"managedBy": "Được quản lý bởi {{provider}}",
|
||||
"memberCount_one": "{{count}} tài khoản",
|
||||
"memberCount_other": "{{count}} tài khoản",
|
||||
@@ -278,6 +290,28 @@
|
||||
"verifyFailed": "Kiểm tra embedding thất bại",
|
||||
"dimensionsMismatch": "{{count}} chiều — không khớp yêu cầu 1536"
|
||||
},
|
||||
"reasoning": {
|
||||
"off": "Off",
|
||||
"offDesc": "No default extended thinking",
|
||||
"auto": "Auto",
|
||||
"autoDesc": "Ask the model to use its own default reasoning effort",
|
||||
"none": "None",
|
||||
"noneDesc": "Explicitly disable provider reasoning while keeping the advanced contract",
|
||||
"minimal": "Minimal",
|
||||
"minimalDesc": "Use the smallest supported reasoning effort above none",
|
||||
"low": "Low",
|
||||
"lowDesc": "~4K token budget",
|
||||
"medium": "Medium",
|
||||
"mediumDesc": "~10-16K token budget",
|
||||
"high": "High",
|
||||
"highDesc": "~32K token budget",
|
||||
"xhigh": "XHigh",
|
||||
"xhighDesc": "Use the maximum supported reasoning effort",
|
||||
"downgrade": "Downgrade",
|
||||
"downgradeDesc": "Normalize to the nearest supported lower level, or disable when no lower supported level exists",
|
||||
"provider_default": "Use Model Default",
|
||||
"provider_defaultDesc": "Let the selected model fall back to its own default reasoning effort"
|
||||
},
|
||||
"toast": {
|
||||
"created": "Đã tạo provider",
|
||||
"createdDesc": "{{name}} đã được thêm",
|
||||
|
||||
@@ -43,6 +43,19 @@
|
||||
"span": {
|
||||
"model": "Model:",
|
||||
"tokens": "Token:",
|
||||
"reasoning": "Reasoning:",
|
||||
"source": "source",
|
||||
"requested": "yêu cầu",
|
||||
"effective": "thực tế",
|
||||
"fallback": "fallback",
|
||||
"modelDefault": "mặc định model",
|
||||
"supportedLevels": "hỗ trợ:",
|
||||
"sourceValue": {
|
||||
"provider_default": "provider default",
|
||||
"reasoning": "agent advanced",
|
||||
"thinking_level": "legacy thinking level",
|
||||
"unset": "unset"
|
||||
},
|
||||
"createdAt": "Tạo lúc:",
|
||||
"input": "Đầu vào:",
|
||||
"output": "Đầu ra:",
|
||||
|
||||
@@ -647,15 +647,48 @@
|
||||
"thinking": {
|
||||
"title": "扩展思考",
|
||||
"description": "允许模型在响应前进行推理。更高级别使用更多令牌,但在复杂任务上产生更好的结果。",
|
||||
"modeLabel": "Reasoning Mode",
|
||||
"inherit": "Use Provider Defaults",
|
||||
"custom": "Custom For This Agent",
|
||||
"customDesc": "Override the provider-owned default reasoning policy for this agent only.",
|
||||
"providerDefaultSummary": "Inherited from {{provider}}",
|
||||
"providerLabelFallback": "provider defaults",
|
||||
"noProviderDefault": "No provider-owned reasoning default is saved yet. Inherit mode will keep this agent on reasoning off until the provider saves one.",
|
||||
"thinkingLevel": "思考级别",
|
||||
"thinkingLevelTip": "面向常见场景的简单预设。需要模型级 effort 控制或明确 fallback 行为时,请开启专家模式。",
|
||||
"expertMode": "专家模式",
|
||||
"expertModeDesc": "请求 none、minimal、auto、xhigh 等模型级 reasoning 级别,并控制所选模型不支持时的处理方式。",
|
||||
"supportedLevelsForModel": "当前模型 {{model}} 显式支持以下 reasoning 级别。",
|
||||
"loadingSupport": "正在加载 {{model}} 的 reasoning 能力元数据。",
|
||||
"unknownSupport": "GoClaw 目前还没有 {{model}} 的显式 reasoning 元数据。",
|
||||
"expertModeUnavailable": "只有当所选模型从 provider models 接口返回了显式能力元数据时,才会显示高级 reasoning 控件。",
|
||||
"modelDefault": "模型默认 reasoning:{{level}}",
|
||||
"requestedEffort": "请求的 Effort",
|
||||
"requestedEffortTip": "GoClaw 按所选模型做规范化之前,用户请求的 reasoning 级别。",
|
||||
"fallbackBehavior": "Fallback 行为",
|
||||
"fallbackBehaviorTip": "当所选模型不支持请求的 effort 时,GoClaw 应该如何处理。",
|
||||
"supportedOption": "此模型支持",
|
||||
"legacyShim": "GoClaw 还会保存一个粗粒度 thinking_level 兼容字段,以便旧版本安全回退。",
|
||||
"off": "关闭",
|
||||
"offDesc": "无扩展思考",
|
||||
"auto": "自动",
|
||||
"autoDesc": "使用模型默认 reasoning 级别",
|
||||
"none": "无",
|
||||
"noneDesc": "显式关闭 provider reasoning,同时保留高级合同",
|
||||
"minimal": "最小",
|
||||
"minimalDesc": "使用高于 none 的最小受支持 reasoning 级别",
|
||||
"low": "低",
|
||||
"lowDesc": "~4K 令牌预算",
|
||||
"medium": "中",
|
||||
"mediumDesc": "~10-16K 令牌预算",
|
||||
"high": "高",
|
||||
"highDesc": "~32K 令牌预算"
|
||||
"highDesc": "~32K 令牌预算",
|
||||
"xhigh": "超高",
|
||||
"xhighDesc": "使用模型支持的最高 reasoning 级别",
|
||||
"downgrade": "降级",
|
||||
"downgradeDesc": "规范化到最近的受支持较低级别,必要时使用最低受支持级别",
|
||||
"provider_default": "使用模型默认值",
|
||||
"provider_defaultDesc": "不强制指定 effort,让 provider/model 使用其默认设置"
|
||||
},
|
||||
"otherConfig": {
|
||||
"title": "其他配置",
|
||||
|
||||
@@ -242,6 +242,17 @@
|
||||
"oauthProviderDefaultHint": "使用下方的默认账户池设置,为所有把这个别名设为主 provider 的 agent 定义可复用的 OpenAI Codex 路由策略。",
|
||||
"oauthRoutingHint": "Agent 页面现在会展示该 agent 的最终生效账户池,以及它是继承这里的默认值还是使用自己的覆盖配置。",
|
||||
"oauthManagedByHint": "这个别名由 provider {{provider}} 管理。请到那个 provider 上编辑池成员,而不是在这个账户上修改。",
|
||||
"reasoningDefaultsTitle": "Reasoning Defaults",
|
||||
"reasoningDefaultsDescription": "Set the default reasoning policy for agents using this provider. Final runtime effort is still normalized against each agent's selected model.",
|
||||
"reasoningPreset": "Default Preset",
|
||||
"reasoningExpertMode": "Expert Defaults",
|
||||
"reasoningExpertModeDesc": "Use provider-owned advanced reasoning defaults when you need efforts such as auto, none, minimal, or xhigh.",
|
||||
"reasoningPreviewDescription": "Previewing explicit reasoning capability metadata for {{model}}.",
|
||||
"reasoningPreviewLabel": "Capability Preview",
|
||||
"reasoningPreviewDefault": "Preview model default reasoning: {{level}}",
|
||||
"reasoningPreviewEmpty": "No explicit reasoning-capable models were returned for this provider yet.",
|
||||
"reasoningRequestedEffort": "Requested Effort",
|
||||
"reasoningFallbackBehavior": "Fallback Behavior",
|
||||
"codexPoolDefaultsTitle": "默认账户池",
|
||||
"codexPoolDefaultsDescription": "为把这个别名作为主 provider 的 agent 定义由该别名拥有的可复用 OpenAI Codex 账户池。",
|
||||
"oauthDisplayNameRecommendation": "如果您希望多个 OpenAI Codex OAuth 账户在池里显示得更清楚,可以补充显示名称。",
|
||||
@@ -258,6 +269,7 @@
|
||||
"poolOwner": "池拥有者",
|
||||
"poolMember": "池成员",
|
||||
"standalone": "独立账户",
|
||||
"reasoningDefault": "Reasoning {{level}}",
|
||||
"managedBy": "由 {{provider}} 管理",
|
||||
"memberCount_one": "{{count}} 个成员",
|
||||
"memberCount_other": "{{count}} 个成员",
|
||||
@@ -296,6 +308,28 @@
|
||||
"verifyFailed": "Embedding验证失败",
|
||||
"dimensionsMismatch": "{{count}}维 — 不符合要求的1536"
|
||||
},
|
||||
"reasoning": {
|
||||
"off": "Off",
|
||||
"offDesc": "No default extended thinking",
|
||||
"auto": "Auto",
|
||||
"autoDesc": "Ask the model to use its own default reasoning effort",
|
||||
"none": "None",
|
||||
"noneDesc": "Explicitly disable provider reasoning while keeping the advanced contract",
|
||||
"minimal": "Minimal",
|
||||
"minimalDesc": "Use the smallest supported reasoning effort above none",
|
||||
"low": "Low",
|
||||
"lowDesc": "~4K token budget",
|
||||
"medium": "Medium",
|
||||
"mediumDesc": "~10-16K token budget",
|
||||
"high": "High",
|
||||
"highDesc": "~32K token budget",
|
||||
"xhigh": "XHigh",
|
||||
"xhighDesc": "Use the maximum supported reasoning effort",
|
||||
"downgrade": "Downgrade",
|
||||
"downgradeDesc": "Normalize to the nearest supported lower level, or disable when no lower supported level exists",
|
||||
"provider_default": "Use Model Default",
|
||||
"provider_defaultDesc": "Let the selected model fall back to its own default reasoning effort"
|
||||
},
|
||||
"toast": {
|
||||
"created": "Provider已创建",
|
||||
"createdDesc": "{{name}} 已添加",
|
||||
|
||||
@@ -46,6 +46,19 @@
|
||||
"endTime": "结束:",
|
||||
"input": "输入:",
|
||||
"model": "模型:",
|
||||
"reasoning": "Reasoning:",
|
||||
"source": "source",
|
||||
"requested": "请求",
|
||||
"effective": "实际",
|
||||
"fallback": "fallback",
|
||||
"modelDefault": "模型默认",
|
||||
"supportedLevels": "支持:",
|
||||
"sourceValue": {
|
||||
"provider_default": "provider default",
|
||||
"reasoning": "agent advanced",
|
||||
"thinking_level": "legacy thinking level",
|
||||
"unset": "unset"
|
||||
},
|
||||
"output": "输出:",
|
||||
"thinking": "思考",
|
||||
"tokens": "令牌:"
|
||||
|
||||
@@ -57,7 +57,7 @@ export function AgentCreateDialog({ open, onOpenChange, onCreate }: AgentCreateD
|
||||
[enabledProviders, provider],
|
||||
);
|
||||
const selectedProviderId = selectedProvider?.id;
|
||||
const { models, loading: modelsLoading } = useProviderModels(selectedProviderId, selectedProvider?.provider_type);
|
||||
const { models, loading: modelsLoading } = useProviderModels(selectedProviderId);
|
||||
const { verify, verifying, result: verifyResult, reset: resetVerify } = useProviderVerify();
|
||||
|
||||
// Reset verification when provider or model changes
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { ConfigGroupHeader } from "@/components/shared/config-group-header";
|
||||
import type {
|
||||
AgentData, ChatGPTOAuthRoutingConfig, CompactionConfig, ContextPruningConfig,
|
||||
ReasoningOverrideMode,
|
||||
SandboxConfig, WorkspaceSharingConfig,
|
||||
} from "@/types/agent";
|
||||
import {
|
||||
@@ -16,13 +17,22 @@ import {
|
||||
} from "./config-sections";
|
||||
import { WorkspaceSection } from "./general-sections";
|
||||
import { useProviders } from "@/pages/providers/hooks/use-providers";
|
||||
import { getChatGPTOAuthProviderRouting } from "@/types/provider";
|
||||
import { useProviderModels } from "@/pages/providers/hooks/use-provider-models";
|
||||
import {
|
||||
getChatGPTOAuthProviderRouting,
|
||||
getProviderReasoningDefaults,
|
||||
normalizeReasoningEffort,
|
||||
normalizeReasoningFallback,
|
||||
deriveLegacyThinkingLevel,
|
||||
} from "@/types/provider";
|
||||
import {
|
||||
buildAgentOtherConfigWithChatGPTOAuthRouting,
|
||||
normalizeChatGPTOAuthRouting,
|
||||
} from "./agent-display-utils";
|
||||
import { buildDraftRouting } from "./codex-pool-routing-draft-utils";
|
||||
|
||||
const SIMPLE_REASONING_LEVELS = new Set(["off", "low", "medium", "high"]);
|
||||
|
||||
interface AgentAdvancedDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
@@ -32,17 +42,50 @@ interface AgentAdvancedDialogProps {
|
||||
|
||||
export function AgentAdvancedDialog({ open, onOpenChange, agent, onUpdate }: AgentAdvancedDialogProps) {
|
||||
const { t } = useTranslation("agents");
|
||||
const { providers } = useProviders();
|
||||
const { providers, loading: providersLoading } = useProviders();
|
||||
const providerByName = new Map(providers.map((provider) => [provider.name, provider]));
|
||||
const currentProvider = providerByName.get(agent.provider);
|
||||
const providerDefaults = getChatGPTOAuthProviderRouting(currentProvider?.settings);
|
||||
const { models: providerModels, loading: providerModelsLoading } = useProviderModels(
|
||||
currentProvider?.id,
|
||||
);
|
||||
const providerRoutingDefaults = getChatGPTOAuthProviderRouting(currentProvider?.settings);
|
||||
const providerReasoningDefaults = getProviderReasoningDefaults(currentProvider?.settings);
|
||||
const currentModelCapability = providerModels.find(
|
||||
(entry) => entry.id === agent.model || agent.model.endsWith(`/${entry.id}`),
|
||||
)?.reasoning ?? null;
|
||||
const expertReasoningAvailable = Boolean(currentModelCapability?.levels?.length);
|
||||
|
||||
const deriveState = (a: AgentData) => {
|
||||
const otherObj = (a.other_config ?? {}) as Record<string, unknown>;
|
||||
const rawReasoning = (otherObj.reasoning ?? {}) as Record<string, unknown>;
|
||||
const rawThinkingLevel = normalizeReasoningEffort(otherObj.thinking_level);
|
||||
const hasReasoningObject = Boolean(otherObj.reasoning) && typeof rawReasoning === "object";
|
||||
const reasoningMode: ReasoningOverrideMode = rawReasoning.override_mode === "inherit"
|
||||
? "inherit"
|
||||
: hasReasoningObject || rawThinkingLevel
|
||||
? "custom"
|
||||
: "inherit";
|
||||
const reasoningEffort = normalizeReasoningEffort(rawReasoning.effort)
|
||||
|| rawThinkingLevel
|
||||
|| providerReasoningDefaults?.effort
|
||||
|| "off";
|
||||
const reasoningFallback = normalizeReasoningFallback(rawReasoning.fallback);
|
||||
const routing = normalizeChatGPTOAuthRouting(a.other_config);
|
||||
const draftRouting = buildDraftRouting(routing);
|
||||
return {
|
||||
thinkingLevel: typeof otherObj.thinking_level === "string" ? otherObj.thinking_level : "off",
|
||||
reasoningMode,
|
||||
thinkingLevel: SIMPLE_REASONING_LEVELS.has(reasoningEffort)
|
||||
? reasoningEffort
|
||||
: deriveLegacyThinkingLevel(reasoningEffort),
|
||||
reasoningEffort,
|
||||
reasoningFallback: reasoningMode === "inherit"
|
||||
? providerReasoningDefaults?.fallback ?? "downgrade"
|
||||
: reasoningFallback,
|
||||
reasoningExpert: reasoningMode === "custom" && (
|
||||
Boolean(otherObj.reasoning)
|
||||
|| !SIMPLE_REASONING_LEVELS.has(reasoningEffort)
|
||||
|| reasoningFallback !== "downgrade"
|
||||
),
|
||||
chatgptRouting: draftRouting,
|
||||
wsSharing: (otherObj.workspace_sharing ?? {}) as WorkspaceSharingConfig,
|
||||
comp: a.compaction_config ?? {},
|
||||
@@ -55,7 +98,11 @@ export function AgentAdvancedDialog({ open, onOpenChange, agent, onUpdate }: Age
|
||||
|
||||
const init = deriveState(agent);
|
||||
const [wsSharing, setWsSharing] = useState<WorkspaceSharingConfig>(init.wsSharing);
|
||||
const [reasoningMode, setReasoningMode] = useState<ReasoningOverrideMode>(init.reasoningMode);
|
||||
const [thinkingLevel, setThinkingLevel] = useState(init.thinkingLevel);
|
||||
const [reasoningEffort, setReasoningEffort] = useState(init.reasoningEffort);
|
||||
const [reasoningFallback, setReasoningFallback] = useState<string>(init.reasoningFallback);
|
||||
const [reasoningExpert, setReasoningExpert] = useState(init.reasoningExpert);
|
||||
const [chatgptRouting, setChatgptRouting] = useState<ChatGPTOAuthRoutingConfig>(init.chatgptRouting);
|
||||
const [comp, setComp] = useState<CompactionConfig>(init.comp);
|
||||
const [pruneEnabled, setPruneEnabled] = useState(init.pruneEnabled);
|
||||
@@ -67,7 +114,11 @@ export function AgentAdvancedDialog({ open, onOpenChange, agent, onUpdate }: Age
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const s = deriveState(agent);
|
||||
setReasoningMode(s.reasoningMode);
|
||||
setThinkingLevel(s.thinkingLevel);
|
||||
setReasoningEffort(s.reasoningEffort);
|
||||
setReasoningFallback(s.reasoningFallback);
|
||||
setReasoningExpert(s.reasoningExpert);
|
||||
setChatgptRouting(s.chatgptRouting);
|
||||
setWsSharing(s.wsSharing);
|
||||
setComp(s.comp);
|
||||
@@ -75,9 +126,35 @@ export function AgentAdvancedDialog({ open, onOpenChange, agent, onUpdate }: Age
|
||||
setPrune(s.prune);
|
||||
setSbEnabled(s.sbEnabled);
|
||||
setSb(s.sb);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !currentProvider || providerModelsLoading) return;
|
||||
if (!expertReasoningAvailable) {
|
||||
if (reasoningExpert) setReasoningExpert(false);
|
||||
if (reasoningFallback !== "downgrade") setReasoningFallback("downgrade");
|
||||
return;
|
||||
}
|
||||
const allowedEfforts = new Set(["off", "auto", ...(currentModelCapability?.levels ?? [])]);
|
||||
if (!allowedEfforts.has(reasoningEffort)) {
|
||||
const fallbackEffort = allowedEfforts.has(thinkingLevel)
|
||||
? thinkingLevel
|
||||
: currentModelCapability?.default_effort ?? "off";
|
||||
setReasoningEffort(fallbackEffort);
|
||||
}
|
||||
}, [
|
||||
currentModelCapability,
|
||||
currentProvider,
|
||||
expertReasoningAvailable,
|
||||
open,
|
||||
providerModelsLoading,
|
||||
reasoningEffort,
|
||||
reasoningExpert,
|
||||
reasoningFallback,
|
||||
thinkingLevel,
|
||||
]);
|
||||
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
@@ -92,9 +169,27 @@ export function AgentAdvancedDialog({ open, onOpenChange, agent, onUpdate }: Age
|
||||
currentProvider?.settings,
|
||||
);
|
||||
delete otherBase.thinking_level;
|
||||
delete otherBase.reasoning;
|
||||
delete otherBase.workspace_sharing;
|
||||
if (thinkingLevel && thinkingLevel !== "off") {
|
||||
otherBase.thinking_level = thinkingLevel;
|
||||
const capabilityResolutionPending = !currentProvider || providersLoading || providerModelsLoading;
|
||||
if (reasoningMode === "inherit") {
|
||||
otherBase.reasoning = {
|
||||
override_mode: "inherit",
|
||||
};
|
||||
} else {
|
||||
const shouldPersistExpertReasoning = reasoningExpert
|
||||
&& (expertReasoningAvailable || capabilityResolutionPending);
|
||||
const requestedEffort = shouldPersistExpertReasoning ? reasoningEffort : thinkingLevel;
|
||||
const legacyThinkingLevel = deriveLegacyThinkingLevel(requestedEffort);
|
||||
if (legacyThinkingLevel !== "off") {
|
||||
otherBase.thinking_level = legacyThinkingLevel;
|
||||
}
|
||||
const reasoningConfig: Record<string, unknown> = {
|
||||
override_mode: "custom",
|
||||
effort: requestedEffort,
|
||||
};
|
||||
if (reasoningFallback !== "downgrade") reasoningConfig.fallback = reasoningFallback;
|
||||
otherBase.reasoning = reasoningConfig;
|
||||
}
|
||||
if (
|
||||
wsSharing.shared_dm || wsSharing.shared_group ||
|
||||
@@ -135,14 +230,52 @@ export function AgentAdvancedDialog({ open, onOpenChange, agent, onUpdate }: Age
|
||||
<WorkspaceSharingSection value={wsSharing} onChange={setWsSharing} />
|
||||
|
||||
{/* Thinking */}
|
||||
<ThinkingSection value={thinkingLevel} onChange={setThinkingLevel} />
|
||||
<ThinkingSection
|
||||
reasoningMode={reasoningMode}
|
||||
thinkingLevel={thinkingLevel}
|
||||
reasoningEffort={reasoningEffort}
|
||||
reasoningFallback={reasoningFallback}
|
||||
expertMode={reasoningExpert}
|
||||
model={agent.model}
|
||||
capability={currentModelCapability}
|
||||
providerDefault={providerReasoningDefaults}
|
||||
providerLabel={currentProvider?.display_name || agent.provider}
|
||||
capabilityLoading={providersLoading || providerModelsLoading}
|
||||
onReasoningModeChange={(mode) => {
|
||||
setReasoningMode(mode);
|
||||
if (mode === "inherit") {
|
||||
setReasoningExpert(false);
|
||||
setReasoningFallback(providerReasoningDefaults?.fallback ?? "downgrade");
|
||||
setReasoningEffort(providerReasoningDefaults?.effort ?? "off");
|
||||
setThinkingLevel(
|
||||
deriveLegacyThinkingLevel(providerReasoningDefaults?.effort ?? "off"),
|
||||
);
|
||||
}
|
||||
}}
|
||||
onThinkingLevelChange={(value) => {
|
||||
setThinkingLevel(value);
|
||||
setReasoningEffort(value);
|
||||
}}
|
||||
onReasoningEffortChange={setReasoningEffort}
|
||||
onReasoningFallbackChange={setReasoningFallback}
|
||||
onExpertModeChange={(enabled) => {
|
||||
setReasoningExpert(enabled);
|
||||
if (!enabled) {
|
||||
const legacy = deriveLegacyThinkingLevel(reasoningEffort);
|
||||
setThinkingLevel(legacy);
|
||||
setReasoningFallback("downgrade");
|
||||
} else if (reasoningEffort === "off" && thinkingLevel !== "off") {
|
||||
setReasoningEffort(thinkingLevel);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<ChatGPTOAuthRoutingSection
|
||||
currentProvider={agent.provider}
|
||||
providers={providers}
|
||||
value={chatgptRouting}
|
||||
onChange={setChatgptRouting}
|
||||
defaultRouting={providerDefaults}
|
||||
defaultRouting={providerRoutingDefaults}
|
||||
membershipEditable={false}
|
||||
membershipManagedByLabel={
|
||||
currentProvider?.display_name || agent.provider
|
||||
|
||||
@@ -27,7 +27,7 @@ export function SubagentsSection({ enabled, value, onToggle, onChange }: Subagen
|
||||
const { providers } = useProviders();
|
||||
const enabledProviders = providers.filter((p) => p.enabled);
|
||||
const defaultProvider = useMemo(() => enabledProviders[0], [enabledProviders]);
|
||||
const { models, loading: modelsLoading } = useProviderModels(defaultProvider?.id, defaultProvider?.provider_type);
|
||||
const { models, loading: modelsLoading } = useProviderModels(defaultProvider?.id);
|
||||
|
||||
return (
|
||||
<ConfigSection
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -6,24 +8,65 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import type { ReasoningOverrideMode } from "@/types/agent";
|
||||
import type { ReasoningCapability } from "@/types/provider";
|
||||
import { InfoLabel } from "./config-section";
|
||||
|
||||
const THINKING_LEVEL_KEYS = ["off", "low", "medium", "high"] as const;
|
||||
const SIMPLE_LEVELS = ["off", "low", "medium", "high"] as const;
|
||||
const FALLBACKS = ["downgrade", "provider_default", "off"] as const;
|
||||
|
||||
interface ThinkingSectionProps {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
reasoningMode: ReasoningOverrideMode;
|
||||
thinkingLevel: string;
|
||||
reasoningEffort: string;
|
||||
reasoningFallback: string;
|
||||
expertMode: boolean;
|
||||
model: string;
|
||||
capability?: ReasoningCapability | null;
|
||||
providerDefault?: {
|
||||
effort?: string;
|
||||
fallback?: "downgrade" | "provider_default" | "off";
|
||||
} | null;
|
||||
providerLabel?: string;
|
||||
capabilityLoading?: boolean;
|
||||
onReasoningModeChange: (v: ReasoningOverrideMode) => void;
|
||||
onThinkingLevelChange: (v: string) => void;
|
||||
onReasoningEffortChange: (v: string) => void;
|
||||
onReasoningFallbackChange: (v: string) => void;
|
||||
onExpertModeChange: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export function ThinkingSection({ value, onChange }: ThinkingSectionProps) {
|
||||
export function ThinkingSection({
|
||||
reasoningMode,
|
||||
thinkingLevel,
|
||||
reasoningEffort,
|
||||
reasoningFallback,
|
||||
expertMode,
|
||||
model,
|
||||
capability,
|
||||
providerDefault,
|
||||
providerLabel,
|
||||
capabilityLoading = false,
|
||||
onReasoningModeChange,
|
||||
onThinkingLevelChange,
|
||||
onReasoningEffortChange,
|
||||
onReasoningFallbackChange,
|
||||
onExpertModeChange,
|
||||
}: ThinkingSectionProps) {
|
||||
const { t } = useTranslation("agents");
|
||||
const s = "configSections.thinking";
|
||||
|
||||
const levels = THINKING_LEVEL_KEYS.map((key) => ({
|
||||
value: key,
|
||||
label: t(`${s}.${key}`),
|
||||
description: t(`${s}.${key}Desc`),
|
||||
}));
|
||||
const supported = new Set(capability?.levels ?? []);
|
||||
const expertAvailable = Boolean(capability?.levels?.length);
|
||||
const advancedEfforts = ["off", "auto", ...(capability?.levels ?? [])];
|
||||
const currentEffort = advancedEfforts.includes(reasoningEffort || "")
|
||||
? reasoningEffort
|
||||
: advancedEfforts.includes(thinkingLevel)
|
||||
? thinkingLevel
|
||||
: capability?.default_effort ?? "off";
|
||||
const inheritedEffort = normalizeInheritedEffort(providerDefault?.effort);
|
||||
const inheritedFallback = providerDefault?.fallback ?? "downgrade";
|
||||
const showCustomControls = reasoningMode === "custom";
|
||||
|
||||
return (
|
||||
<section className="space-y-3">
|
||||
@@ -33,26 +76,190 @@ export function ThinkingSection({ value, onChange }: ThinkingSectionProps) {
|
||||
{t(`${s}.description`)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<InfoLabel tip="Thinking level controls the token budget for reasoning. Anthropic uses budget_tokens, OpenAI uses reasoning_effort, DashScope uses thinking_budget. Token budgets vary by provider.">
|
||||
{t(`${s}.thinkingLevel`)}
|
||||
</InfoLabel>
|
||||
<Select value={value || "off"} onValueChange={onChange}>
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{levels.map((level) => (
|
||||
<SelectItem key={level.value} value={level.value}>
|
||||
<span>{level.label}</span>
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
{level.description}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<section className="space-y-2.5">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{t(`${s}.modeLabel`)}
|
||||
</p>
|
||||
<div className="grid gap-1.5 xl:grid-cols-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant={reasoningMode === "inherit" ? "default" : "outline"}
|
||||
onClick={() => onReasoningModeChange("inherit")}
|
||||
className="h-9"
|
||||
>
|
||||
{t(`${s}.inherit`)}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={reasoningMode === "custom" ? "default" : "outline"}
|
||||
onClick={() => onReasoningModeChange("custom")}
|
||||
className="h-9"
|
||||
>
|
||||
{t(`${s}.custom`)}
|
||||
</Button>
|
||||
</div>
|
||||
{reasoningMode === "inherit" ? (
|
||||
providerDefault ? (
|
||||
<div className="rounded-lg border px-3 py-3 text-sm">
|
||||
<p className="font-medium">
|
||||
{t(`${s}.providerDefaultSummary`, {
|
||||
provider: providerLabel || t(`${s}.providerLabelFallback`),
|
||||
})}
|
||||
</p>
|
||||
<div className="mt-2 flex flex-wrap gap-2 text-xs text-muted-foreground">
|
||||
<Badge variant="secondary">{t(`${s}.${inheritedEffort}`)}</Badge>
|
||||
<Badge variant="outline">{t(`${s}.${inheritedFallback}`)}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed px-3 py-3 text-sm text-muted-foreground">
|
||||
{t(`${s}.noProviderDefault`)}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(`${s}.customDesc`)}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{showCustomControls ? (
|
||||
<div className="space-y-2">
|
||||
<InfoLabel tip={t(`${s}.thinkingLevelTip`)}>
|
||||
{t(`${s}.thinkingLevel`)}
|
||||
</InfoLabel>
|
||||
<Select
|
||||
value={thinkingLevel || "off"}
|
||||
onValueChange={(value) => {
|
||||
onThinkingLevelChange(value);
|
||||
onReasoningEffortChange(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-56">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SIMPLE_LEVELS.map((level) => (
|
||||
<SelectItem key={level} value={level}>
|
||||
<span>{t(`${s}.${level}`)}</span>
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
{t(`${s}.${level}Desc`)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="rounded-md border p-3">
|
||||
{expertAvailable && showCustomControls ? (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium">{t(`${s}.expertMode`)}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(`${s}.expertModeDesc`)}
|
||||
</p>
|
||||
</div>
|
||||
<Switch checked={expertMode} onCheckedChange={onExpertModeChange} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-3 space-y-2 text-xs text-muted-foreground">
|
||||
<p>
|
||||
{capability?.levels?.length
|
||||
? t(`${s}.supportedLevelsForModel`, { model })
|
||||
: capabilityLoading
|
||||
? t(`${s}.loadingSupport`, { model })
|
||||
: t(`${s}.unknownSupport`, { model })}
|
||||
</p>
|
||||
{capability?.levels?.length ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{capability.levels.map((level) => (
|
||||
<Badge key={level} variant="outline" className="text-[10px]">
|
||||
{t(`${s}.${level}`)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{capability?.default_effort ? (
|
||||
<p>
|
||||
{t(`${s}.modelDefault`, {
|
||||
level: t(`${s}.${capability.default_effort}`),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
{!expertAvailable && !capabilityLoading ? (
|
||||
<p>{t(`${s}.expertModeUnavailable`)}</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{expertAvailable && expertMode && showCustomControls ? (
|
||||
<div className="mt-4 space-y-3 border-t pt-3">
|
||||
<div className="space-y-2">
|
||||
<InfoLabel tip={t(`${s}.requestedEffortTip`)}>
|
||||
{t(`${s}.requestedEffort`)}
|
||||
</InfoLabel>
|
||||
<Select
|
||||
value={currentEffort}
|
||||
onValueChange={onReasoningEffortChange}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-72">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{advancedEfforts.map((effort) => (
|
||||
<SelectItem key={effort} value={effort}>
|
||||
<span>{t(`${s}.${effort}`)}</span>
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
{effort !== "off" && effort !== "auto" && supported.has(effort)
|
||||
? t(`${s}.supportedOption`)
|
||||
: t(`${s}.${effort}Desc`)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<InfoLabel tip={t(`${s}.fallbackBehaviorTip`)}>
|
||||
{t(`${s}.fallbackBehavior`)}
|
||||
</InfoLabel>
|
||||
<Select
|
||||
value={reasoningFallback}
|
||||
onValueChange={onReasoningFallbackChange}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-72">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FALLBACKS.map((fallback) => (
|
||||
<SelectItem key={fallback} value={fallback}>
|
||||
<span>{t(`${s}.${fallback}`)}</span>
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
{t(`${s}.${fallback}Desc`)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(`${s}.legacyShim`)}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeInheritedEffort(value: string | undefined): string {
|
||||
if (!value) return "off";
|
||||
return [
|
||||
"off", "auto", "none", "minimal", "low", "medium", "high", "xhigh",
|
||||
].includes(value) ? value : "off";
|
||||
}
|
||||
@@ -154,7 +154,6 @@ function SortableProviderCard({ entry, index, toolName, enabledProviders, onUpda
|
||||
const selectedProvider = enabledProviders.find((p) => p.id === entry.provider_id);
|
||||
const { models, loading: modelsLoading } = useProviderModels(
|
||||
entry.provider_id || undefined,
|
||||
selectedProvider?.provider_type,
|
||||
);
|
||||
|
||||
const paramSchema: ParamField[] =
|
||||
|
||||
@@ -1,40 +1,30 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useHttp } from "@/hooks/use-ws";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import type { ModelInfo } from "@/types/provider";
|
||||
import type { ModelInfo, ProviderModelsResponse } from "@/types/provider";
|
||||
|
||||
export type { ModelInfo };
|
||||
|
||||
// Hardcoded models for the OpenAI Codex OAuth provider.
|
||||
// OAuth token lacks api.model.read scope, so we can't call /v1/models.
|
||||
const CODEX_MODELS: ModelInfo[] = [
|
||||
{ id: "gpt-5.4", name: "GPT-5.4" },
|
||||
{ id: "gpt-5.4-mini", name: "GPT-5.4 Mini" },
|
||||
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
|
||||
{ id: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark" },
|
||||
{ id: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
|
||||
{ id: "gpt-5.2", name: "GPT-5.2" },
|
||||
{ id: "gpt-5.1-codex", name: "GPT-5.1 Codex" },
|
||||
{ id: "gpt-5.1-codex-max", name: "GPT-5.1 Codex Max" },
|
||||
{ id: "gpt-5.1-codex-mini", name: "GPT-5.1 Codex Mini" },
|
||||
{ id: "gpt-5.1", name: "GPT-5.1" },
|
||||
];
|
||||
|
||||
export function useProviderModels(providerId: string | undefined, providerType?: string) {
|
||||
export function useProviderModels(providerId: string | undefined) {
|
||||
const http = useHttp();
|
||||
const isOAuth = providerType === "chatgpt_oauth";
|
||||
|
||||
const { data: models = [], isLoading: loading } = useQuery({
|
||||
const {
|
||||
data,
|
||||
isLoading: loading,
|
||||
} = useQuery({
|
||||
queryKey: queryKeys.providers.models(providerId ?? ""),
|
||||
queryFn: async () => {
|
||||
if (isOAuth) return CODEX_MODELS;
|
||||
const res = await http.get<{ models: ModelInfo[] }>(
|
||||
const res = await http.get<ProviderModelsResponse>(
|
||||
`/v1/providers/${providerId}/models`,
|
||||
);
|
||||
return res.models ?? [];
|
||||
return res;
|
||||
},
|
||||
enabled: !!providerId,
|
||||
});
|
||||
|
||||
return { models, loading };
|
||||
return {
|
||||
models: data?.models ?? [],
|
||||
reasoningDefaults: data?.reasoning_defaults ?? null,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
@@ -10,18 +10,31 @@ import {
|
||||
import { StickySaveBar } from "@/components/shared/sticky-save-bar";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PROVIDER_TYPES } from "@/constants/providers";
|
||||
import { toast } from "@/stores/use-toast-store";
|
||||
import { useProviders } from "../hooks/use-providers";
|
||||
import { useProviderModels } from "../hooks/use-provider-models";
|
||||
import { useProviderVerify } from "../hooks/use-provider-verify";
|
||||
import { ProviderOAuthAccountSection } from "./provider-oauth-account-section";
|
||||
import {
|
||||
buildProviderSettingsWithChatGPTOAuthRouting,
|
||||
buildProviderSettingsWithReasoningDefaults,
|
||||
getChatGPTOAuthProviderRouting,
|
||||
getEmbeddingSettings,
|
||||
getProviderReasoningDefaults,
|
||||
normalizeReasoningEffort,
|
||||
normalizeReasoningFallback,
|
||||
deriveLegacyThinkingLevel,
|
||||
} from "@/types/provider";
|
||||
import type { ProviderData, ProviderInput } from "@/types/provider";
|
||||
import type { ChatGPTOAuthRoutingConfig } from "@/types/agent";
|
||||
@@ -50,6 +63,9 @@ const NO_EMBEDDING_TYPES = new Set([
|
||||
"suno",
|
||||
"anthropic_native",
|
||||
]);
|
||||
const SIMPLE_REASONING_LEVELS = new Set(["off", "low", "medium", "high"]);
|
||||
const ADVANCED_REASONING_LEVELS = ["off", "auto", "none", "minimal", "low", "medium", "high", "xhigh"] as const;
|
||||
const REASONING_FALLBACKS = ["downgrade", "provider_default", "off"] as const;
|
||||
|
||||
function providerStatus(
|
||||
providerName: string,
|
||||
@@ -97,6 +113,8 @@ function providerFormSignature(input: {
|
||||
embModel: string;
|
||||
embApiBase: string;
|
||||
routing: ChatGPTOAuthRoutingConfig;
|
||||
reasoningEffort: string;
|
||||
reasoningFallback: string;
|
||||
isOAuth: boolean;
|
||||
}): string {
|
||||
return JSON.stringify({
|
||||
@@ -107,6 +125,15 @@ function providerFormSignature(input: {
|
||||
embModel: input.embModel,
|
||||
embApiBase: input.embApiBase,
|
||||
routing: input.isOAuth ? routingSignature(input.routing) : "",
|
||||
reasoning: reasoningSignature(input.reasoningEffort, input.reasoningFallback),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function reasoningSignature(effort: string, fallback: string): string {
|
||||
return JSON.stringify({
|
||||
effort: normalizeReasoningEffort(effort) || "off",
|
||||
fallback: normalizeReasoningFallback(fallback),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -114,6 +141,10 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
const { t } = useTranslation("providers");
|
||||
const { t: tc } = useTranslation("common");
|
||||
const { providers } = useProviders();
|
||||
const {
|
||||
models: providerModels,
|
||||
reasoningDefaults: providerReasoningDefaults,
|
||||
} = useProviderModels(provider.id);
|
||||
const { statuses } = useChatGPTOAuthProviderStatuses(providers);
|
||||
|
||||
const typeInfo = PROVIDER_TYPES.find((pt) => pt.value === provider.provider_type);
|
||||
@@ -163,6 +194,11 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
);
|
||||
|
||||
const initialRouting = getChatGPTOAuthProviderRouting(provider.settings);
|
||||
const initialReasoningDefaults = getProviderReasoningDefaults(provider.settings)
|
||||
?? providerReasoningDefaults
|
||||
?? null;
|
||||
const initialReasoningEffort = initialReasoningDefaults?.effort ?? "off";
|
||||
const initialReasoningFallback = initialReasoningDefaults?.fallback ?? "downgrade";
|
||||
|
||||
const [displayName, setDisplayName] = useState(provider.display_name || "");
|
||||
const [apiKey, setApiKey] = useState(provider.api_key || "");
|
||||
@@ -189,7 +225,45 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
const [embEnabled, setEmbEnabled] = useState(initEmb?.enabled ?? false);
|
||||
const [embModel, setEmbModel] = useState(initEmb?.model ?? "");
|
||||
const [embApiBase, setEmbApiBase] = useState(initEmb?.api_base ?? "");
|
||||
const [reasoningThinkingLevel, setReasoningThinkingLevel] = useState(
|
||||
deriveLegacyThinkingLevel(initialReasoningEffort),
|
||||
);
|
||||
const [reasoningEffort, setReasoningEffort] = useState(initialReasoningEffort);
|
||||
const [reasoningFallback, setReasoningFallback] = useState(initialReasoningFallback);
|
||||
const [reasoningExpert, setReasoningExpert] = useState(
|
||||
Boolean(initialReasoningDefaults) && (
|
||||
!SIMPLE_REASONING_LEVELS.has(initialReasoningEffort)
|
||||
|| initialReasoningFallback !== "downgrade"
|
||||
),
|
||||
);
|
||||
const [reasoningPreviewModel, setReasoningPreviewModel] = useState("");
|
||||
const syncedProviderIDRef = useRef(provider.id);
|
||||
const reasoningCapableModels = useMemo(
|
||||
() => providerModels.filter((model) => (model.reasoning?.levels?.length ?? 0) > 0),
|
||||
[providerModels],
|
||||
);
|
||||
const reasoningPreviewEntry = useMemo(
|
||||
() => reasoningCapableModels.find((model) => model.id === reasoningPreviewModel)
|
||||
?? reasoningCapableModels[0]
|
||||
?? null,
|
||||
[reasoningCapableModels, reasoningPreviewModel],
|
||||
);
|
||||
const reasoningPreviewCapability = reasoningPreviewEntry?.reasoning ?? null;
|
||||
const showReasoningDefaults = Boolean(initialReasoningDefaults) || reasoningCapableModels.length > 0;
|
||||
const savedReasoningSignature = useMemo(
|
||||
() => reasoningSignature(
|
||||
initialReasoningDefaults?.effort ?? "off",
|
||||
initialReasoningDefaults?.fallback ?? "downgrade",
|
||||
),
|
||||
[initialReasoningDefaults?.effort, initialReasoningDefaults?.fallback],
|
||||
);
|
||||
const draftReasoningSignature = useMemo(
|
||||
() => reasoningSignature(
|
||||
reasoningExpert ? reasoningEffort : reasoningThinkingLevel,
|
||||
reasoningExpert ? reasoningFallback : "downgrade",
|
||||
),
|
||||
[reasoningEffort, reasoningExpert, reasoningFallback, reasoningThinkingLevel],
|
||||
);
|
||||
|
||||
const savedFormSignature = useMemo(
|
||||
() =>
|
||||
@@ -206,9 +280,11 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
strategy: initialRouting?.strategy ?? "primary_first",
|
||||
extra_provider_names: initialRouting?.extraProviderNames ?? [],
|
||||
},
|
||||
reasoningEffort: initialReasoningDefaults?.effort ?? "off",
|
||||
reasoningFallback: initialReasoningDefaults?.fallback ?? "downgrade",
|
||||
isOAuth,
|
||||
}),
|
||||
[initEmb?.api_base, initEmb?.enabled, initEmb?.model, initialRouting?.extraProviderNames, initialRouting?.strategy, isOAuth, provider.api_key, provider.display_name, provider.enabled, showApiKey],
|
||||
[initEmb?.api_base, initEmb?.enabled, initEmb?.model, initialReasoningDefaults?.effort, initialReasoningDefaults?.fallback, initialRouting?.extraProviderNames, initialRouting?.strategy, isOAuth, provider.api_key, provider.display_name, provider.enabled, showApiKey],
|
||||
);
|
||||
const savedFormSignatureRef = useRef(savedFormSignature);
|
||||
const draftFormSignature = useMemo(
|
||||
@@ -223,15 +299,18 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
embModel,
|
||||
embApiBase,
|
||||
routing: poolRouting,
|
||||
reasoningEffort: reasoningExpert ? reasoningEffort : reasoningThinkingLevel,
|
||||
reasoningFallback: reasoningExpert ? reasoningFallback : "downgrade",
|
||||
isOAuth,
|
||||
}),
|
||||
[apiKey, displayName, embApiBase, embEnabled, embModel, enabled, isOAuth, poolRouting, provider.api_key, showApiKey],
|
||||
[apiKey, displayName, embApiBase, embEnabled, embModel, enabled, isOAuth, poolRouting, provider.api_key, reasoningEffort, reasoningExpert, reasoningFallback, reasoningThinkingLevel, showApiKey],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const nextProviderID = provider.id;
|
||||
const es = getEmbeddingSettings(provider.settings);
|
||||
const routing = getChatGPTOAuthProviderRouting(provider.settings);
|
||||
const reasoning = getProviderReasoningDefaults(provider.settings) ?? providerReasoningDefaults;
|
||||
const syncFromProvider = () => {
|
||||
setEmbEnabled(es?.enabled ?? false);
|
||||
setEmbModel(es?.model ?? "");
|
||||
@@ -240,6 +319,15 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
strategy: routing?.strategy ?? "primary_first",
|
||||
extra_provider_names: routing?.extraProviderNames ?? [],
|
||||
});
|
||||
const nextReasoningEffort = reasoning?.effort ?? "off";
|
||||
const nextReasoningFallback = reasoning?.fallback ?? "downgrade";
|
||||
setReasoningEffort(nextReasoningEffort);
|
||||
setReasoningFallback(nextReasoningFallback);
|
||||
setReasoningThinkingLevel(deriveLegacyThinkingLevel(nextReasoningEffort));
|
||||
setReasoningExpert(
|
||||
!SIMPLE_REASONING_LEVELS.has(nextReasoningEffort)
|
||||
|| nextReasoningFallback !== "downgrade",
|
||||
);
|
||||
setDisplayName(provider.display_name || "");
|
||||
setApiKey(provider.api_key || "");
|
||||
setEnabled(provider.enabled);
|
||||
@@ -261,7 +349,18 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
syncFromProvider();
|
||||
}
|
||||
savedFormSignatureRef.current = savedFormSignature;
|
||||
}, [draftFormSignature, provider.api_key, provider.display_name, provider.enabled, provider.id, provider.settings, savedFormSignature]);
|
||||
}, [draftFormSignature, provider.api_key, provider.display_name, provider.enabled, provider.id, provider.settings, providerReasoningDefaults, savedFormSignature]);
|
||||
|
||||
useEffect(() => {
|
||||
if (reasoningCapableModels.length === 0) {
|
||||
if (reasoningPreviewModel !== "") setReasoningPreviewModel("");
|
||||
return;
|
||||
}
|
||||
if (reasoningCapableModels.some((model) => model.id === reasoningPreviewModel)) {
|
||||
return;
|
||||
}
|
||||
setReasoningPreviewModel(reasoningCapableModels[0]?.id ?? "");
|
||||
}, [reasoningCapableModels, reasoningPreviewModel]);
|
||||
|
||||
const quotaProviderNames = useMemo(
|
||||
() => {
|
||||
@@ -363,6 +462,15 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
poolRouting,
|
||||
);
|
||||
}
|
||||
nextSettings = buildProviderSettingsWithReasoningDefaults(
|
||||
nextSettings,
|
||||
showReasoningDefaults
|
||||
? {
|
||||
effort: reasoningExpert ? reasoningEffort : reasoningThinkingLevel,
|
||||
fallback: reasoningExpert ? reasoningFallback : "downgrade",
|
||||
}
|
||||
: null,
|
||||
);
|
||||
data.settings = nextSettings;
|
||||
|
||||
await onUpdate(provider.id, data);
|
||||
@@ -401,7 +509,8 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
strategy: initialRouting?.strategy ?? "primary_first",
|
||||
extra_provider_names: initialRouting?.extraProviderNames ?? [],
|
||||
});
|
||||
const isDirty = displayNameDirty || enabledDirty || apiKeyDirty || embeddingDirty || routingDirty;
|
||||
const reasoningDirty = draftReasoningSignature !== savedReasoningSignature;
|
||||
const isDirty = displayNameDirty || enabledDirty || apiKeyDirty || embeddingDirty || routingDirty || reasoningDirty;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -453,6 +562,156 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{showReasoningDefaults ? (
|
||||
<section className="space-y-4 rounded-lg border p-3 sm:p-4 overflow-hidden">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-medium">{t("detail.reasoningDefaultsTitle")}</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("detail.reasoningDefaultsDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t("detail.reasoningPreset")}</Label>
|
||||
<Select
|
||||
value={reasoningThinkingLevel}
|
||||
onValueChange={(value) => {
|
||||
setReasoningThinkingLevel(value);
|
||||
setReasoningEffort(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-56">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(["off", "low", "medium", "high"] as const).map((level) => (
|
||||
<SelectItem key={level} value={level}>
|
||||
<span>{t(`reasoning.${level}`)}</span>
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
{t(`reasoning.${level}Desc`)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium">{t("detail.reasoningExpertMode")}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("detail.reasoningExpertModeDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={reasoningExpert}
|
||||
onCheckedChange={(enabled) => {
|
||||
setReasoningExpert(enabled);
|
||||
if (!enabled) {
|
||||
const legacy = deriveLegacyThinkingLevel(reasoningEffort);
|
||||
setReasoningThinkingLevel(legacy);
|
||||
setReasoningFallback("downgrade");
|
||||
} else if (reasoningEffort === "off" && reasoningThinkingLevel !== "off") {
|
||||
setReasoningEffort(reasoningThinkingLevel);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 space-y-2 text-xs text-muted-foreground">
|
||||
{reasoningPreviewEntry ? (
|
||||
<>
|
||||
<p>
|
||||
{t("detail.reasoningPreviewDescription", {
|
||||
model: reasoningPreviewEntry.name || reasoningPreviewEntry.id,
|
||||
})}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="reasoningPreviewModel">{t("detail.reasoningPreviewLabel")}</Label>
|
||||
<Select value={reasoningPreviewEntry.id} onValueChange={setReasoningPreviewModel}>
|
||||
<SelectTrigger id="reasoningPreviewModel" className="w-full sm:w-72">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{reasoningCapableModels.map((model) => (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
{model.name || model.id}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{reasoningPreviewCapability?.levels?.length ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{reasoningPreviewCapability.levels.map((level) => (
|
||||
<Badge key={level} variant="outline" className="text-[10px]">
|
||||
{t(`reasoning.${level}`)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{reasoningPreviewCapability?.default_effort ? (
|
||||
<p>
|
||||
{t("detail.reasoningPreviewDefault", {
|
||||
level: t(`reasoning.${reasoningPreviewCapability.default_effort}`),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<p>{t("detail.reasoningPreviewEmpty")}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{reasoningExpert ? (
|
||||
<div className="mt-4 space-y-3 border-t pt-3">
|
||||
<div className="space-y-2">
|
||||
<Label>{t("detail.reasoningRequestedEffort")}</Label>
|
||||
<Select value={reasoningEffort} onValueChange={setReasoningEffort}>
|
||||
<SelectTrigger className="w-full sm:w-72">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ADVANCED_REASONING_LEVELS.map((effort) => (
|
||||
<SelectItem key={effort} value={effort}>
|
||||
<span>{t(`reasoning.${effort}`)}</span>
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
{t(`reasoning.${effort}Desc`)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t("detail.reasoningFallbackBehavior")}</Label>
|
||||
<Select
|
||||
value={reasoningFallback}
|
||||
onValueChange={(value) => setReasoningFallback(normalizeReasoningFallback(value))}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-72">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{REASONING_FALLBACKS.map((fallback) => (
|
||||
<SelectItem key={fallback} value={fallback}>
|
||||
<span>{t(`reasoning.${fallback}`)}</span>
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
{t(`reasoning.${fallback}Desc`)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{canEditPoolRouting ? (
|
||||
<ChatGPTOAuthRoutingSection
|
||||
title={t("detail.codexPoolDefaultsTitle")}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChatGPTOAuthQuotaStrip } from "@/pages/agents/agent-detail/chatgpt-oauth-quota-strip";
|
||||
import type { EffectiveChatGPTOAuthRoutingStrategy } from "@/types/agent";
|
||||
import { getProviderReasoningDefaults } from "@/types/provider";
|
||||
import type { ChatGPTOAuthProviderQuota } from "./hooks/use-chatgpt-oauth-provider-quotas";
|
||||
import type { ChatGPTOAuthAvailability } from "./hooks/use-chatgpt-oauth-provider-statuses";
|
||||
import type { ProviderData } from "./hooks/use-providers";
|
||||
@@ -70,6 +71,7 @@ export function ProviderListRow({
|
||||
: null;
|
||||
const showQuota = provider.provider_type === "chatgpt_oauth"
|
||||
&& (oauthPool?.quotaLoading || Boolean(oauthPool?.quota));
|
||||
const reasoningDefaults = getProviderReasoningDefaults(provider.settings);
|
||||
const connectorLineClass = oauthPool?.connectorPosition === "first" || oauthPool?.connectorPosition === "middle"
|
||||
? "top-[-0.75rem] h-[calc(100%+1.5rem)]"
|
||||
: "top-[-0.75rem] h-[calc(50%+0.75rem)]";
|
||||
@@ -137,6 +139,13 @@ export function ProviderListRow({
|
||||
{t(oauthPool.role === "owner" ? "list.poolOwner" : "list.poolMember")}
|
||||
</Badge>
|
||||
)}
|
||||
{reasoningDefaults ? (
|
||||
<Badge variant="secondary" className="h-5 px-1.5 text-[10px]">
|
||||
{t("list.reasoningDefault", {
|
||||
level: t(`reasoning.${reasoningDefaults.effort ?? "off"}`),
|
||||
})}
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
{(secondaryText || availabilityWarningLabel || showQuota) && (
|
||||
<div className="flex min-w-0 items-center gap-2 text-xs">
|
||||
|
||||
@@ -23,7 +23,7 @@ interface StepModelProps {
|
||||
|
||||
export function StepModel({ provider, onComplete, onBack, initialModel }: StepModelProps) {
|
||||
const { t } = useTranslation("setup");
|
||||
const { models, loading: modelsLoading } = useProviderModels(provider.id, provider.provider_type);
|
||||
const { models, loading: modelsLoading } = useProviderModels(provider.id);
|
||||
const { verify, verifying, result: verifyResult, reset: resetVerify } = useProviderVerify();
|
||||
|
||||
const [model, setModel] = useState(initialModel ?? "");
|
||||
|
||||
@@ -385,6 +385,42 @@ function SpanTreeNode({ node, depth }: { node: SpanNode; depth: number }) {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{span.metadata?.reasoning && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<span>{t("span.reasoning")}</span>{" "}
|
||||
{span.metadata.reasoning.requested_effort ? (
|
||||
<span>
|
||||
{t("span.requested")} {span.metadata.reasoning.requested_effort}
|
||||
</span>
|
||||
) : null}
|
||||
{span.metadata.reasoning.source ? (
|
||||
<span className="ml-2">
|
||||
{t("span.source")} {t(`span.sourceValue.${span.metadata.reasoning.source}`)}
|
||||
</span>
|
||||
) : null}
|
||||
{span.metadata.reasoning.effective_effort ? (
|
||||
<span className="ml-2">
|
||||
{t("span.effective")} {span.metadata.reasoning.effective_effort}
|
||||
</span>
|
||||
) : null}
|
||||
{span.metadata.reasoning.fallback ? (
|
||||
<span className="ml-2">
|
||||
{t("span.fallback")} {span.metadata.reasoning.fallback}
|
||||
</span>
|
||||
) : null}
|
||||
{span.metadata.reasoning.used_provider_default ? (
|
||||
<span className="ml-2">{t("span.modelDefault")}</span>
|
||||
) : null}
|
||||
{span.metadata.reasoning.reason ? (
|
||||
<div className="mt-1">{span.metadata.reasoning.reason}</div>
|
||||
) : null}
|
||||
{span.metadata.reasoning.supported_levels?.length ? (
|
||||
<div className="mt-1">
|
||||
{t("span.supportedLevels")} {span.metadata.reasoning.supported_levels.join(", ")}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{span.input_preview && (
|
||||
<PreviewBlock label={t("span.input")} content={span.input_preview} />
|
||||
)}
|
||||
|
||||
@@ -88,6 +88,13 @@ export type EffectiveChatGPTOAuthRoutingStrategy =
|
||||
| "priority_order";
|
||||
|
||||
export type ChatGPTOAuthRoutingOverrideMode = "inherit" | "custom";
|
||||
export type ReasoningOverrideMode = "inherit" | "custom";
|
||||
|
||||
export interface AgentReasoningConfig {
|
||||
override_mode?: ReasoningOverrideMode;
|
||||
effort?: string;
|
||||
fallback?: "downgrade" | "provider_default" | "off";
|
||||
}
|
||||
|
||||
export interface ChatGPTOAuthRoutingConfig {
|
||||
override_mode?: ChatGPTOAuthRoutingOverrideMode;
|
||||
|
||||
@@ -29,6 +29,22 @@ export interface ProviderInput {
|
||||
export interface ModelInfo {
|
||||
id: string;
|
||||
name?: string;
|
||||
reasoning?: ReasoningCapability;
|
||||
}
|
||||
|
||||
export interface ProviderReasoningDefaults {
|
||||
effort?: string;
|
||||
fallback?: "downgrade" | "provider_default" | "off";
|
||||
}
|
||||
|
||||
export interface ProviderModelsResponse {
|
||||
models: ModelInfo[];
|
||||
reasoning_defaults?: ProviderReasoningDefaults;
|
||||
}
|
||||
|
||||
export interface ReasoningCapability {
|
||||
levels?: string[];
|
||||
default_effort?: string;
|
||||
}
|
||||
|
||||
export interface EmbeddingSettings {
|
||||
@@ -69,6 +85,53 @@ export function normalizeChatGPTOAuthStrategy(
|
||||
return "primary_first";
|
||||
}
|
||||
|
||||
export function normalizeReasoningEffort(value: unknown): string {
|
||||
if (typeof value !== "string") return "";
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return [
|
||||
"off", "auto", "none", "minimal", "low", "medium", "high", "xhigh",
|
||||
].includes(normalized) ? normalized : "";
|
||||
}
|
||||
|
||||
export function normalizeReasoningFallback(
|
||||
value: unknown,
|
||||
): "downgrade" | "provider_default" | "off" {
|
||||
if (value === "provider_default" || value === "off") {
|
||||
return value;
|
||||
}
|
||||
return "downgrade";
|
||||
}
|
||||
|
||||
/** Maps advanced reasoning effort levels to the legacy three-tier thinking_level. */
|
||||
export function deriveLegacyThinkingLevel(effort: string): string {
|
||||
switch (effort) {
|
||||
case "low":
|
||||
case "medium":
|
||||
case "high":
|
||||
return effort;
|
||||
case "minimal":
|
||||
return "low";
|
||||
case "xhigh":
|
||||
return "high";
|
||||
default:
|
||||
return "off";
|
||||
}
|
||||
}
|
||||
|
||||
export function getProviderReasoningDefaults(
|
||||
settings?: Record<string, unknown>,
|
||||
): ProviderReasoningDefaults | null {
|
||||
const raw = settings?.reasoning_defaults;
|
||||
if (!raw || typeof raw !== "object") return null;
|
||||
const reasoning = raw as Record<string, unknown>;
|
||||
const effort = normalizeReasoningEffort(reasoning.effort) || "off";
|
||||
const fallback = normalizeReasoningFallback(reasoning.fallback);
|
||||
if (effort === "off" && fallback === "downgrade") {
|
||||
return null;
|
||||
}
|
||||
return { effort, fallback };
|
||||
}
|
||||
|
||||
export function getChatGPTOAuthProviderRouting(
|
||||
settings?: Record<string, unknown>,
|
||||
): NormalizedChatGPTOAuthProviderRouting | null {
|
||||
@@ -104,3 +167,22 @@ export function buildProviderSettingsWithChatGPTOAuthRouting(
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
export function buildProviderSettingsWithReasoningDefaults(
|
||||
settings: Record<string, unknown> | undefined,
|
||||
reasoning: ProviderReasoningDefaults | null,
|
||||
): Record<string, unknown> {
|
||||
const next: Record<string, unknown> = { ...(settings ?? {}) };
|
||||
const effort = normalizeReasoningEffort(reasoning?.effort) || "off";
|
||||
const fallback = normalizeReasoningFallback(reasoning?.fallback);
|
||||
|
||||
delete next.reasoning_defaults;
|
||||
if (effort !== "off" || fallback !== "downgrade") {
|
||||
next.reasoning_defaults = {
|
||||
effort,
|
||||
fallback,
|
||||
};
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
@@ -47,6 +47,20 @@ export interface SpanData {
|
||||
tool_call_id: string;
|
||||
input_preview: string;
|
||||
output_preview: string;
|
||||
metadata?: { cache_creation_tokens?: number; cache_read_tokens?: number; thinking_tokens?: number };
|
||||
metadata?: {
|
||||
cache_creation_tokens?: number;
|
||||
cache_read_tokens?: number;
|
||||
thinking_tokens?: number;
|
||||
reasoning?: {
|
||||
source?: string;
|
||||
requested_effort?: string;
|
||||
effective_effort?: string;
|
||||
fallback?: string;
|
||||
reason?: string;
|
||||
known_model?: boolean;
|
||||
supported_levels?: string[];
|
||||
used_provider_default?: boolean;
|
||||
};
|
||||
};
|
||||
created_at: string;
|
||||
}
|
||||
Reference in new issue
Block a user