From 3f87816b3c7748ff4bd432615738aba6fa3e631a Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Mon, 19 Oct 2026 16:32:35 +0000 Subject: [PATCH 1/5] Map model token limits to native settings --- .../internal/agent/claudesdk/local_test.go | 5 ++++- .../internal/agent/claudesdk/provider.go | 13 +++++++++++-- .../internal/agent/claudesdk/workspace.go | 3 ++- .../internal/agent/codex/model_route_test.go | 15 +++++++++++++++ apps/daemon/internal/agent/codex/options.go | 5 ++++- .../internal/agent/codex/provider_config.go | 2 ++ .../features/overview/getting-started.test.ts | 2 +- .../features/system/ModelProviderDialog.tsx | 4 ++-- contracts/agents-api/core-errors.md | 2 +- contracts/agents-api/core.openapi.yaml | 4 ++++ contracts/agents-api/model-execution.md | 16 +++++++++------- contracts/agents-api/v1/model_configuration.go | 2 ++ contracts/agents-api/v1/model_execution.go | 3 +++ .../agents-api/v1/model_execution_test.go | 4 ++-- .../agents-api/v1/model_provider_error_test.go | 1 + .../agents-api/v1/saved_core_extension.go | 6 +++++- .../agents-api/v1/saved_core_extension_test.go | 4 ++-- contracts/agents-api/zh/core-errors.md | 4 ++-- contracts/agents-api/zh/model-execution.md | 18 ++++++++++-------- internal/harnessconfig/builtin/route_test.go | 2 +- .../harnessconfig/claudesdk/declaration.go | 2 ++ internal/harnessconfig/codex/declaration.go | 2 ++ internal/harnessconfig/harness.go | 3 +++ internal/harnessconfig/mcode/declaration.go | 2 +- internal/harnessconfig/provider.go | 17 ++++++++++++++++- .../agents-client/src/admin-client.test.ts | 4 ++-- packages/agents-client/src/admin-projection.ts | 3 ++- .../agents-client/src/generated/core-api.ts | 3 ++- .../src/provider-observations.test.ts | 4 ++-- packages/claude-sdk-adapter/src/workspace.ts | 3 ++- .../internal/api/harness_model_providers.go | 2 +- 31 files changed, 118 insertions(+), 42 deletions(-) diff --git a/apps/daemon/internal/agent/claudesdk/local_test.go b/apps/daemon/internal/agent/claudesdk/local_test.go index 8ad7f47a1..2df47293c 100644 --- a/apps/daemon/internal/agent/claudesdk/local_test.go +++ b/apps/daemon/internal/agent/claudesdk/local_test.go @@ -40,7 +40,7 @@ func TestWorkspaceProviderCredentialsReplaceAmbientSelection(t *testing.T) { config := workspaceFixture(t) original := slices.Clone(config.Env) req := workspaceRequest() - req.AgentOptions["model_provider"] = map[string]any{"protocol": "anthropic", "base_url": "https://provider.example/anthropic", "api_key": "selected-secret"} + req.AgentOptions["model_provider"] = map[string]any{"protocol": "anthropic", "base_url": "https://provider.example/anthropic", "api_key": "selected-secret", "context_window": 64000, "max_output_tokens": 4096} start, env, err := prepare(config, req) if err != nil { t.Fatal(err) @@ -52,6 +52,9 @@ func TestWorkspaceProviderCredentialsReplaceAmbientSelection(t *testing.T) { if !slices.Contains(env, "ANTHROPIC_AUTH_TOKEN=selected-secret") || slices.Contains(env, "ANTHROPIC_AUTH_TOKEN=selected-provider-fixture") { t.Fatal("provider selection was not exclusive") } + if !slices.Contains(env, "CLAUDE_CODE_MAX_CONTEXT_TOKENS=64000") || !slices.Contains(env, "CLAUDE_CODE_MAX_OUTPUT_TOKENS=4096") { + t.Fatal("token limits were not native") + } for _, value := range []any{nil, "secret", map[string]any{"protocol": "anthropic", "base_url": "http://provider.example", "api_key": "secret"}, map[string]any{"protocol": "anthropic", "base_url": "https://user:pass@provider.example", "api_key": "secret"}} { req.AgentOptions["model_provider"] = value if _, _, err := prepare(config, req); err == nil || strings.Contains(err.Error(), "secret") { diff --git a/apps/daemon/internal/agent/claudesdk/provider.go b/apps/daemon/internal/agent/claudesdk/provider.go index 85c1036ca..e6f8ff9ca 100644 --- a/apps/daemon/internal/agent/claudesdk/provider.go +++ b/apps/daemon/internal/agent/claudesdk/provider.go @@ -1,6 +1,7 @@ package claudesdk import ( + "strconv" "strings" harnessconfiguration "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/claudesdk" @@ -12,7 +13,15 @@ func providerEnvironment(value any) ([]string, error) { if err != nil { return nil, err } - return []string{"ANTHROPIC_BASE_URL=" + provider.BaseURL, "ANTHROPIC_AUTH_TOKEN=" + provider.APIKey}, nil + env := []string{"ANTHROPIC_BASE_URL=" + provider.BaseURL, "ANTHROPIC_AUTH_TOKEN=" + provider.APIKey} + // Native uses the context window only for a model it does not know. + if provider.ContextWindow > 0 { + env = append(env, "CLAUDE_CODE_MAX_CONTEXT_TOKENS="+strconv.Itoa(int(provider.ContextWindow))) + } + if provider.MaxOutputTokens > 0 { + env = append(env, "CLAUDE_CODE_MAX_OUTPUT_TOKENS="+strconv.Itoa(int(provider.MaxOutputTokens))) + } + return env, nil } func withProvider(env, provider []string) []string { @@ -22,7 +31,7 @@ func withProvider(env, provider []string) []string { out := make([]string, 0, len(env)+len(provider)) for _, entry := range env { key, _, _ := strings.Cut(entry, "=") - if key != "ANTHROPIC_API_KEY" && key != "ANTHROPIC_AUTH_TOKEN" && key != "ANTHROPIC_BASE_URL" { + if key != "ANTHROPIC_API_KEY" && key != "ANTHROPIC_AUTH_TOKEN" && key != "ANTHROPIC_BASE_URL" && key != "CLAUDE_CODE_MAX_CONTEXT_TOKENS" && key != "CLAUDE_CODE_MAX_OUTPUT_TOKENS" { out = append(out, entry) } } diff --git a/apps/daemon/internal/agent/claudesdk/workspace.go b/apps/daemon/internal/agent/claudesdk/workspace.go index de032d717..f8fda8e1b 100644 --- a/apps/daemon/internal/agent/claudesdk/workspace.go +++ b/apps/daemon/internal/agent/claudesdk/workspace.go @@ -124,7 +124,8 @@ func workspaceEnvName(name string) bool { switch name { case "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL", "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL", - "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY": + "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS", "CLAUDE_CODE_MAX_CONTEXT_TOKENS", "CLAUDE_CODE_MAX_OUTPUT_TOKENS", + "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY": return true default: return false diff --git a/apps/daemon/internal/agent/codex/model_route_test.go b/apps/daemon/internal/agent/codex/model_route_test.go index c18fecfe9..74ad45684 100644 --- a/apps/daemon/internal/agent/codex/model_route_test.go +++ b/apps/daemon/internal/agent/codex/model_route_test.go @@ -2,6 +2,7 @@ package codex import ( "reflect" + "slices" "strings" "testing" ) @@ -35,3 +36,17 @@ func TestPlanRejectsIncompleteExplicitProvider(t *testing.T) { } } } + +func TestPlanMapsTheContextWindowNatively(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + provider := map[string]any{"protocol": "responses", "base_url": "https://model.example/v1", "api_key": "fixture", "context_window": 64000} + plan, err := BuildSessionPlan(testStateRoot(t), "limits", map[string]any{"model": "m", "model_provider": provider}, nil) + if err != nil || !slices.Contains(plan.ExtraConfig, [2]string{"model_context_window", "64000"}) { + t.Fatalf("context window not native: %v %v", plan.ExtraConfig, err) + } + plan.Cleanup() + provider["max_output_tokens"] = 4096 + if _, err := BuildSessionPlan(testStateRoot(t), "limits", map[string]any{"model": "m", "model_provider": provider}, nil); err == nil { + t.Fatal("max_output_tokens without a native home accepted") + } +} diff --git a/apps/daemon/internal/agent/codex/options.go b/apps/daemon/internal/agent/codex/options.go index 154cd671c..af94a53b5 100644 --- a/apps/daemon/internal/agent/codex/options.go +++ b/apps/daemon/internal/agent/codex/options.go @@ -123,6 +123,9 @@ func BuildSessionPlan(stateRoot, agentStateKey string, opts map[string]any, cont plan.DisableFeatures = append(plan.DisableFeatures, "shell_snapshot") env = append(env, providerAPIKeyEnv+"="+provider.BearerToken) excludeFromShellEnvironment(&plan, providerAPIKeyEnv) + if provider.ContextWindow > 0 { + plan.ExtraConfig = append(plan.ExtraConfig, [2]string{"model_context_window", fmt.Sprint(provider.ContextWindow)}) + } } plan.Env = env @@ -258,7 +261,7 @@ func normaliseProviderConfig(raw any) (providerConfig, bool, error) { if err != nil { return providerConfig{}, false, err } - return providerConfig{BaseURL: provider.BaseURL, BearerToken: provider.APIKey, WireAPI: "responses"}, true, nil + return providerConfig{BaseURL: provider.BaseURL, BearerToken: provider.APIKey, WireAPI: "responses", ContextWindow: provider.ContextWindow}, true, nil } func stringOpt(opts map[string]any, key string) string { diff --git a/apps/daemon/internal/agent/codex/provider_config.go b/apps/daemon/internal/agent/codex/provider_config.go index bfdfe692f..4f5b95df3 100644 --- a/apps/daemon/internal/agent/codex/provider_config.go +++ b/apps/daemon/internal/agent/codex/provider_config.go @@ -42,6 +42,8 @@ type providerConfig struct { // WireAPI is "responses" (codex Responses API) or "chat" (rejected // upstream since codex-rs 0.140). Empty defaults to "responses". WireAPI string + // ContextWindow is the native model_context_window when set. + ContextWindow int32 } // writeCodexProviderConfig writes (or appends to) /config.toml diff --git a/apps/web/src/features/overview/getting-started.test.ts b/apps/web/src/features/overview/getting-started.test.ts index 76ae44806..927960707 100644 --- a/apps/web/src/features/overview/getting-started.test.ts +++ b/apps/web/src/features/overview/getting-started.test.ts @@ -12,7 +12,7 @@ const deployment = (overrides: Partial = {}): SandboxDeployme const fleet = (value: SandboxDeployment, nodes = [node("n1")]): FleetState => ({ status: "ready", snapshot: { deployment: value, nodes, allocations: [], loadedAt: 0 }, targetGeneration: value.generation, refreshing: false, error: null }); const sandboxes = (state: FleetState) => gettingStartedSteps({ sandboxReset: false, fleet: state, localOnly: false, projects: [], sessions: 0, harnesses: [] }).sandboxes; const provider = { object: "core.model_configuration", model: "fixture-model", harness_config: {}, model_provider: { protocol: "responses", base_url: "https://model.example/v1", api_key_configured: true }, last_used_at: null, last_error_code: null, last_error_at: null, updated_at: "2026-09-25T00:00:00Z" } as const; -const harness = (id: CoreHarness["id"], fields: Partial = {}): CoreHarness => ({ object: "core.harness", model_configuration_support: { protocols: ["responses"], accepts_harness_config: true }, id, enabled: true, default: false, model_configuration: null, ...fields }); +const harness = (id: CoreHarness["id"], fields: Partial = {}): CoreHarness => ({ object: "core.harness", model_configuration_support: { protocols: ["responses"], accepts_harness_config: true, accepts_max_output_tokens: false }, id, enabled: true, default: false, model_configuration: null, ...fields }); describe("Getting started steps", () => { it("uses live provider readiness independently of target state or a durable pin", () => { diff --git a/apps/web/src/features/system/ModelProviderDialog.tsx b/apps/web/src/features/system/ModelProviderDialog.tsx index 89439a50f..6ff1a410c 100644 --- a/apps/web/src/features/system/ModelProviderDialog.tsx +++ b/apps/web/src/features/system/ModelProviderDialog.tsx @@ -83,7 +83,7 @@ export function ModelProviderDialog({ harness, onClose, onSaved, onReread }: { const url = baseUrl.trim(); const urlProblem = url && !isProviderUrl(url) ? t("models.form.baseUrlInvalid") : null; const context = tokenLimit(contextWindow); - const output = tokenLimit(maxOutputTokens); + const output = tokenLimit(support?.accepts_max_output_tokens ? maxOutputTokens : ""); const limitProblem = (limit: ReturnType) => (limit.problem === "whole" ? t("models.form.wholeNumber") : limit.problem === "large" ? t("models.form.tooLarge") : null); const outputProblem = limitProblem(output); // An undersized window is the field the administrator must fix. @@ -220,7 +220,7 @@ export function ModelProviderDialog({ harness, onClose, onSaved, onReread }: { : null}
{limitField("context", contextWindow, setContextWindow, contextProblem)} - {limitField("output", maxOutputTokens, setMaxOutputTokens, outputProblem)} + {support?.accepts_max_output_tokens ? limitField("output", maxOutputTokens, setMaxOutputTokens, outputProblem) : null}
diff --git a/contracts/agents-api/core-errors.md b/contracts/agents-api/core-errors.md index 25a3e21a0..306d6d18a 100644 --- a/contracts/agents-api/core-errors.md +++ b/contracts/agents-api/core-errors.md @@ -61,7 +61,7 @@ Each code returns HTTP 400 with `type: "invalid_request_error"`. A missing, malf | `model_provider_base_url_invalid` | `base_url` | omitted | Requires HTTPS without credentials, query or fragment | | `model_provider_protocol_unsupported` | `protocol` | `harness` and `allowed_protocols`, from the build's adapter catalog | The protocol is unknown or unsupported by the selected Harness | | `model_provider_api_key_invalid` | `api_key` | `max_length`: 16384 | The key is empty, too long or contains a prohibited character | -| `model_provider_token_limits_invalid` | `context_window` or `max_output_tokens` | omitted | A limit is negative, or `max_output_tokens` exceeds a set `context_window` | +| `model_provider_token_limits_invalid` | `context_window` or `max_output_tokens` | omitted | A limit is negative, `max_output_tokens` exceeds a set `context_window`, or the Harness has no native setting for a set limit | | `model_configuration_model_invalid` | `model` | omitted | The deployment default's model is not a nonempty model identifier | | `harness_config_invalid` | `harness_config` | omitted | The deployment default's native parameters are unsupported or invalid | | `invalid_sandbox_configuration` | `resources.cpus` | `min`: 1, `max`: 255 | The CPU count is outside the supported bounds | diff --git a/contracts/agents-api/core.openapi.yaml b/contracts/agents-api/core.openapi.yaml index 1c360662d..a38349cfd 100644 --- a/contracts/agents-api/core.openapi.yaml +++ b/contracts/agents-api/core.openapi.yaml @@ -2537,12 +2537,16 @@ definitions: properties: accepts_harness_config: type: boolean + accepts_max_output_tokens: + description: AcceptsMaxOutputTokens reports that the Harness maps max_output_tokens natively. + type: boolean protocols: items: type: string type: array required: - accepts_harness_config + - accepts_max_output_tokens - protocols type: object v1.ModelProviderInput: diff --git a/contracts/agents-api/model-execution.md b/contracts/agents-api/model-execution.md index 8e31c33b3..159fd06af 100644 --- a/contracts/agents-api/model-execution.md +++ b/contracts/agents-api/model-execution.md @@ -15,13 +15,15 @@ A Harness receives one frozen model provider bundle, defined by [`modelprovider. | `api_key` | Nonempty, at most 16 KiB, without NUL, CR or LF | | `context_window`, `max_output_tokens` | Optional nonnegative integers, with output no larger than context. Use the real model's limits | -Supported protocols by Harness: +Supported protocols and native token limits by Harness: -| Harness | Supported protocols, default first | -| --- | --- | -| Codex | `responses` | -| Claude Code | `anthropic` | -| MiniMax Code | `anthropic`, `responses`, `chat_completions` | +| Harness | Supported protocols, default first | Native token limits | +| --- | --- | --- | +| Codex | `responses` | `model_context_window`; no output limit | +| Claude Code | `anthropic` | `CLAUDE_CODE_MAX_CONTEXT_TOKENS`, which native uses only for a model it does not know, and `CLAUDE_CODE_MAX_OUTPUT_TOKENS`, which native caps at the model's limit | +| MiniMax Code | `anthropic`, `responses`, `chat_completions` | The custom model's `limit.context` and `limit.output` | + +A set limit with no native setting returns `model_provider_token_limits_invalid`. Core validates the resolved combination before writing the Session. Core and Runtime read the same ordered `protocols` declaration in `internal/harnessconfig`. There is no model API proxy, passthrough gateway or cross-protocol conversion, including inside a Harness. Unsupported saved configurations and Session snapshots fail when used; they are never rewritten, aliased or migrated. @@ -132,7 +134,7 @@ PUT requires `model` and a complete `model_provider`; `harness_config` defaults Session creation decrypts the default of the resolved Harness and freezes it in the Session's encrypted snapshot like any other bundle, so changing or removing the default never reaches existing Sessions. The execution-configuration read shows the frozen safe view with source `deployment`. A missing or invalid provider snapshot fails closed, with no fallback to another model or provider. -`model_configuration_support` derives from the adapter declaration that Core and Runtime share: `protocols` lists the selectable native protocols with the default first, and `accepts_harness_config` says whether native parameters are accepted. It describes the build, not a live Runtime or a remote model. +`model_configuration_support` derives from the adapter declaration that Core and Runtime share: `protocols` lists the selectable native protocols with the default first, `accepts_harness_config` says whether native parameters are accepted, and `accepts_max_output_tokens` whether `max_output_tokens` has a native setting. It describes the build, not a live Runtime or a remote model. ### Deployment default observations diff --git a/contracts/agents-api/v1/model_configuration.go b/contracts/agents-api/v1/model_configuration.go index 7aeb9926a..d5c5d5f4c 100644 --- a/contracts/agents-api/v1/model_configuration.go +++ b/contracts/agents-api/v1/model_configuration.go @@ -59,6 +59,8 @@ func ValidateNativeModelConfiguration(harness, model string, raw json.RawMessage type ModelConfigurationSupport struct { Protocols []string `json:"protocols" binding:"required"` AcceptsHarnessConfig bool `json:"accepts_harness_config" binding:"required"` + // AcceptsMaxOutputTokens reports that the Harness maps max_output_tokens natively. + AcceptsMaxOutputTokens bool `json:"accepts_max_output_tokens" binding:"required"` } func (p *ModelProviderInput) ValidateConfiguration(harness string, native json.RawMessage) error { diff --git a/contracts/agents-api/v1/model_execution.go b/contracts/agents-api/v1/model_execution.go index c66f516b8..8cc80a5a2 100644 --- a/contracts/agents-api/v1/model_execution.go +++ b/contracts/agents-api/v1/model_execution.go @@ -75,5 +75,8 @@ func (p *ModelProviderInput) ValidateHarnessWithRegistry(harness string, registr if err := configuration.ValidateProtocol(string(p.Protocol)); err != nil { return &ModelProviderError{Code: "model_provider_protocol_unsupported", Param: "protocol", message: err.Error()} } + if err := configuration.ValidateMaxOutputTokens(p.MaxOutputTokens); err != nil { + return &ModelProviderError{Code: "model_provider_token_limits_invalid", Param: "max_output_tokens", message: err.Error()} + } return nil } diff --git a/contracts/agents-api/v1/model_execution_test.go b/contracts/agents-api/v1/model_execution_test.go index fb6c5237e..b6ddb0526 100644 --- a/contracts/agents-api/v1/model_execution_test.go +++ b/contracts/agents-api/v1/model_execution_test.go @@ -13,7 +13,7 @@ func TestModelExecutionValidation(t *testing.T) { t.Run(harness+"/"+protocol, func(t *testing.T) { p := ModelProviderInput{Protocol: modelprovider.Protocol(protocol), BaseURL: "https://example.com/v1", APIKey: "secret", ContextWindow: 200000, MaxOutputTokens: 8000} native := harness == "mcode" || (harness == "codex" && protocol == "responses") || (harness == "claude_sdk" && protocol == "anthropic") - if err := p.ValidateHarness(harness); (err == nil) != native { + if err := p.ValidateHarness(harness); (err == nil) != (native && harness != "codex") { t.Fatalf("wrong native protocol admission: %v", err) } p.ContextWindow, p.MaxOutputTokens = 0, 0 @@ -55,7 +55,7 @@ func TestProviderDeclarationsAgreeWithAdmission(t *testing.T) { for _, protocol := range []string{"responses", "anthropic", "unknown"} { _, supported := declared.Provider(protocol) input := ModelProviderInput{Protocol: modelprovider.Protocol(protocol), BaseURL: "https://example.test", APIKey: "private-fixture", ContextWindow: 100, MaxOutputTokens: 20} - if (input.ValidateHarness(kind) == nil) != supported { + if (input.ValidateHarness(kind) == nil) != (supported && declared.MaxOutputTokens) { t.Fatalf("protocol %q disagrees with validation", protocol) } if !supported { diff --git a/contracts/agents-api/v1/model_provider_error_test.go b/contracts/agents-api/v1/model_provider_error_test.go index b00738a33..0b264d6e4 100644 --- a/contracts/agents-api/v1/model_provider_error_test.go +++ b/contracts/agents-api/v1/model_provider_error_test.go @@ -20,6 +20,7 @@ func TestModelProviderErrorMessagesAndPrecedence(t *testing.T) { {"key", "claude_sdk", "model_provider_api_key_invalid", "api_key", "invalid model provider API key", func(p *ModelProviderInput) { p.Protocol = "anthropic"; p.APIKey = "" }}, {"context before output", "codex", "model_provider_token_limits_invalid", "context_window", "invalid model token limits", func(p *ModelProviderInput) { p.ContextWindow = -1; p.MaxOutputTokens = -2 }}, {"output", "codex", "model_provider_token_limits_invalid", "max_output_tokens", "invalid model token limits", func(p *ModelProviderInput) { p.ContextWindow = 1; p.MaxOutputTokens = 2 }}, + {"no native output limit", "codex", "model_provider_token_limits_invalid", "max_output_tokens", "The selected harness has no native max_output_tokens setting.", func(p *ModelProviderInput) { p.ContextWindow = 10; p.MaxOutputTokens = 1 }}, } { t.Run(tc.name, func(t *testing.T) { p := valid diff --git a/contracts/agents-api/v1/saved_core_extension.go b/contracts/agents-api/v1/saved_core_extension.go index af8969a25..d5977ba6f 100644 --- a/contracts/agents-api/v1/saved_core_extension.go +++ b/contracts/agents-api/v1/saved_core_extension.go @@ -79,5 +79,9 @@ func (p *ModelProviderView) ValidateHarnessWithRegistry(harness string, registry if p == nil { return errors.New("model_provider is required") } - return registry.ValidateProtocol(harness, string(p.Protocol)) + if err := registry.ValidateProtocol(harness, string(p.Protocol)); err != nil { + return err + } + configuration, _ := registry.Lookup(harness) + return configuration.ValidateMaxOutputTokens(p.MaxOutputTokens) } diff --git a/contracts/agents-api/v1/saved_core_extension_test.go b/contracts/agents-api/v1/saved_core_extension_test.go index 5c30ace79..bf823c5df 100644 --- a/contracts/agents-api/v1/saved_core_extension_test.go +++ b/contracts/agents-api/v1/saved_core_extension_test.go @@ -11,7 +11,7 @@ import ( func TestSavedProviderSafeView(t *testing.T) { input := &SavedAgentCoreInput{Harness: "codex", ModelProvider: &ModelProviderInput{ Protocol: "responses", BaseURL: "https://example.test/v1", APIKey: "write-only-fixture", - ContextWindow: 200000, MaxOutputTokens: 8000, + ContextWindow: 200000, }} if err := input.Validate(); err != nil { t.Fatal(err) @@ -34,7 +34,7 @@ func TestSavedProviderExplicitHarnessCompatibility(t *testing.T) { for _, protocol := range []string{"anthropic", "responses", "chat_completions"} { t.Run(harness+"/"+protocol, func(t *testing.T) { x := &SavedAgentCoreInput{Harness: harness, ModelProvider: &ModelProviderInput{ - Protocol: modelprovider.Protocol(protocol), BaseURL: "https://example.test", APIKey: "fixture", ContextWindow: 100, MaxOutputTokens: 20, + Protocol: modelprovider.Protocol(protocol), BaseURL: "https://example.test", APIKey: "fixture", ContextWindow: 100, }} native := harness == "" || harness == "mcode" || (harness == "codex" && protocol == "responses") || (harness == "claude_sdk" && protocol == "anthropic") if err := x.Validate(); (err == nil) != native { diff --git a/contracts/agents-api/zh/core-errors.md b/contracts/agents-api/zh/core-errors.md index a14595b16..99373de2f 100644 --- a/contracts/agents-api/zh/core-errors.md +++ b/contracts/agents-api/zh/core-errors.md @@ -1,7 +1,7 @@ --- title: "Core 管理错误" source: contracts/agents-api/core-errors.md -source_hash: 8799a6f1ba80217f77e7adac3412e60482be731fcea665dfb9ec19fad92a8d54 +source_hash: 814b5faa5534df0814f8037a947c24bc1f4d78960aff9f5c5bf42fe4d1ad11ec --- `/core/v1` 上的错误使用此封装结构。`message` 是安全的英文文本;`code` 和 `param` 可以为 null。客户端依据稳定的 `code` 和可选的 `param` 进行处理,对未知代码显示 `message`,绝不解析消息,也绝不自动重试被拒绝的写操作。 @@ -63,7 +63,7 @@ Web 的控制台服务器在 `/core` 路径上发生自身故障时使用此封 | `model_provider_base_url_invalid` | `base_url` | 省略 | 必须使用 HTTPS,且不得包含凭据、查询或片段 | | `model_provider_protocol_unsupported` | `protocol` | `harness` 和 `allowed_protocols`,来自该构建的适配器目录 | 协议未知,或所选 Harness 不支持该协议 | | `model_provider_api_key_invalid` | `api_key` | `max_length`:16384 | 密钥为空、过长或包含禁止字符 | -| `model_provider_token_limits_invalid` | `context_window` 或 `max_output_tokens` | 省略 | 限制为负数,或 `max_output_tokens` 超过已设置的 `context_window` | +| `model_provider_token_limits_invalid` | `context_window` 或 `max_output_tokens` | 省略 | 限制为负数、`max_output_tokens` 超过已设置的 `context_window`,或 Harness 没有已设置限制的原生设置 | | `model_configuration_model_invalid` | `model` | 省略 | 部署默认配置的 model 不是非空模型标识符 | | `harness_config_invalid` | `harness_config` | 省略 | 部署默认配置的原生参数不受支持或无效 | | `invalid_sandbox_configuration` | `resources.cpus` | `min`:1,`max`:255 | CPU 数量超出支持范围 | diff --git a/contracts/agents-api/zh/model-execution.md b/contracts/agents-api/zh/model-execution.md index 5bbc565a2..c496c0797 100644 --- a/contracts/agents-api/zh/model-execution.md +++ b/contracts/agents-api/zh/model-execution.md @@ -1,7 +1,7 @@ --- title: "模型执行" source: contracts/agents-api/model-execution.md -source_hash: 5486e6c406aa220d5897a2e1980b4f432bd62fe4718317842370d89f66736b0c +source_hash: 5ebda69322721893a97cb6814ea72df3ca8c02d1fc9b59724e5392f8e2583db9 --- 每个 Session 都运行一个 Harness,并使用一个模型提供商。Core 通过三个固定版本上游协议未定义的 Core 扩展来选择它们:`x_agents_core.harness` 选择 Harness,`x_agents_core.model_provider` 提供端点和密钥,`x_agents_core.harness_config` 携带原生模型参数。Core 没有提供商目录、模型别名解析或产品权限模型;除 Session 和已保存 Agent 配置包外,唯一存储的配置包是每个 Harness 的一个 [deployment default](#deployment-defaults)。本文档定义 Harness—模型提供商协议:[`internal/modelprovider/config.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/modelprovider/config.go) 负责验证冻结的提供商连接,每个 Harness 则通过 [`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go) 声明其协议和原生参数。 @@ -17,13 +17,15 @@ Harness 接收一个冻结的模型提供商配置包,其定义见 [`modelprov | `api_key` | 不得为空,最长 16 KiB,不得包含 NUL、CR 或 LF | | `context_window`、`max_output_tokens` | 可选的非负整数,输出限制不得大于上下文限制。请使用真实模型的限制 | -各 Harness 支持的协议: +各 Harness 支持的协议和原生 token 限制: -| Harness | 支持的协议(默认项在前) | -| --- | --- | -| Codex | `responses` | -| Claude Code | `anthropic` | -| MiniMax Code | `anthropic`、`responses`、`chat_completions` | +| Harness | 支持的协议(默认项在前) | 原生 token 限制 | +| --- | --- | --- | +| Codex | `responses` | `model_context_window`;无输出限制 | +| Claude Code | `anthropic` | `CLAUDE_CODE_MAX_CONTEXT_TOKENS`(原生仅对其不认识的模型使用)和 `CLAUDE_CODE_MAX_OUTPUT_TOKENS`(原生将其限制在模型上限内) | +| MiniMax Code | `anthropic`、`responses`、`chat_completions` | 自定义模型的 `limit.context` 和 `limit.output` | + +已设置但没有原生设置的限制返回 `model_provider_token_limits_invalid`。 Core 会在写入 Session 前验证解析后的组合。Core 和 Runtime 读取 `internal/harnessconfig` 中相同的有序 `protocols` 声明。不存在模型 API 代理、直通网关或跨协议转换,Harness 内部也不例外。不受支持的已保存配置和 Session 快照一旦使用便会失败;它们绝不会在何处被重写、创建别名或迁移。 @@ -134,7 +136,7 @@ PUT 要求提供 `model` 和完整的 `model_provider`;`harness_config` 默认 创建 Session 时,Core 会解密解析后 Harness 的默认值,并像处理其他配置包一样将其冻结在 Session 的加密快照中,因此更改或移除默认值绝不会影响现有 Session。execution-configuration 读取结果会显示冻结的安全视图,其来源为 `deployment`。提供商快照缺失或无效时会安全失败,并且不会回退到其他模型或提供商。 -`model_configuration_support` 派生自 Core 和 Runtime 共享的适配器声明:`protocols` 列出可选择的原生协议,并将默认项放在首位;`accepts_harness_config` 表示是否接受原生参数。它描述的是构建版本,而不是实时 Runtime 或远程模型。 +`model_configuration_support` 派生自 Core 和 Runtime 共享的适配器声明:`protocols` 列出可选择的原生协议,并将默认项放在首位;`accepts_harness_config` 表示是否接受原生参数;`accepts_max_output_tokens` 表示 `max_output_tokens` 是否有原生设置。它描述的是构建版本,而不是实时 Runtime 或远程模型。 ### 部署默认值观测 {#deployment-default-observations} diff --git a/internal/harnessconfig/builtin/route_test.go b/internal/harnessconfig/builtin/route_test.go index 6a074700f..8ea329bae 100644 --- a/internal/harnessconfig/builtin/route_test.go +++ b/internal/harnessconfig/builtin/route_test.go @@ -25,7 +25,7 @@ func TestNativeProviderDeclarationIsSharedWithRuntime(t *testing.T) { t.Fatal("native configuration support drift") } for _, protocol := range []string{"anthropic", "responses", "chat_completions"} { - raw := map[string]any{"protocol": protocol, "base_url": "https://model.example/v1", "api_key": "key-canary", "context_window": 64000, "max_output_tokens": 4096} + raw := map[string]any{"protocol": protocol, "base_url": "https://model.example/v1", "api_key": "key-canary", "context_window": 64000} p, err := config.ParseProvider(raw) accepted := kind == "mcode" || (kind == "codex" && protocol == "responses") || (kind == "claude_sdk" && protocol == "anthropic") if (err == nil) != accepted || (config.ValidateProtocol(protocol) == nil) != accepted { diff --git a/internal/harnessconfig/claudesdk/declaration.go b/internal/harnessconfig/claudesdk/declaration.go index d3978e937..b724eae8b 100644 --- a/internal/harnessconfig/claudesdk/declaration.go +++ b/internal/harnessconfig/claudesdk/declaration.go @@ -20,6 +20,8 @@ var Declaration = harnessconfig.Declaration{ Configuration: harnessconfig.Configuration{ ValidateNativeConfig: validateNativeConfig, Providers: []harnessconfig.Provider{{Protocol: "anthropic"}}, + // CLAUDE_CODE_MAX_OUTPUT_TOKENS, which native caps at the model's limit. + MaxOutputTokens: true, }, Capabilities: proto.AgentKindCapabilities{ SubagentObservations: proto.CapabilitySupported, diff --git a/internal/harnessconfig/codex/declaration.go b/internal/harnessconfig/codex/declaration.go index 5b708aaa6..bb1492a73 100644 --- a/internal/harnessconfig/codex/declaration.go +++ b/internal/harnessconfig/codex/declaration.go @@ -16,6 +16,8 @@ var Declaration = harnessconfig.Declaration{ Kind: "codex", Label: "Codex", Configuration: harnessconfig.Configuration{ + // context_window maps to model_context_window; Codex has no native + // max_output_tokens setting. Providers: []harnessconfig.Provider{{Protocol: "responses"}}, }, Capabilities: proto.AgentKindCapabilities{ diff --git a/internal/harnessconfig/harness.go b/internal/harnessconfig/harness.go index 66dd2d895..b52259cd7 100644 --- a/internal/harnessconfig/harness.go +++ b/internal/harnessconfig/harness.go @@ -54,6 +54,9 @@ type Provider struct { // only for direct adapters whose model connection remains native-owned. type Configuration struct { Providers []Provider + // MaxOutputTokens reports that the adapter maps a provider's + // max_output_tokens to a native setting. Every adapter maps context_window. + MaxOutputTokens bool // ValidateNativeConfig belongs to the selected adapter, never Core. ValidateNativeConfig func(proto.HarnessConfig) bool } diff --git a/internal/harnessconfig/mcode/declaration.go b/internal/harnessconfig/mcode/declaration.go index a2c6a3baf..b340eea49 100644 --- a/internal/harnessconfig/mcode/declaration.go +++ b/internal/harnessconfig/mcode/declaration.go @@ -23,7 +23,7 @@ var Declaration = harnessconfig.Declaration{ {Protocol: "anthropic"}, {Protocol: "responses"}, {Protocol: "chat_completions"}, - }}, + }, MaxOutputTokens: true}, Capabilities: proto.AgentKindCapabilities{ SubagentObservations: proto.CapabilitySupported, Streaming: proto.CapabilitySupported, diff --git a/internal/harnessconfig/provider.go b/internal/harnessconfig/provider.go index 794501705..cada8d39b 100644 --- a/internal/harnessconfig/provider.go +++ b/internal/harnessconfig/provider.go @@ -1,6 +1,10 @@ package harnessconfig -import "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" +import ( + "errors" + + "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" +) func (c Configuration) AcceptsHarnessConfig() bool { return c.ValidateNativeConfig != nil } @@ -14,5 +18,16 @@ func (c Configuration) ParseProvider(raw any) (modelprovider.Provider, error) { if err := c.ValidateProtocol(string(provider.Protocol)); err != nil { return modelprovider.Provider{}, err } + if err := c.ValidateMaxOutputTokens(provider.MaxOutputTokens); err != nil { + return modelprovider.Provider{}, err + } return provider, nil } + +// ValidateMaxOutputTokens rejects a set limit the Harness has no native home for. +func (c Configuration) ValidateMaxOutputTokens(value int32) error { + if value > 0 && !c.MaxOutputTokens { + return errors.New("The selected harness has no native max_output_tokens setting.") + } + return nil +} diff --git a/packages/agents-client/src/admin-client.test.ts b/packages/agents-client/src/admin-client.test.ts index 58b0e8395..b1205cbd6 100644 --- a/packages/agents-client/src/admin-client.test.ts +++ b/packages/agents-client/src/admin-client.test.ts @@ -156,8 +156,8 @@ describe("AdminClient transport boundary", () => { it("manages deployment default model providers without ever reading a key", async () => { const provider = { last_used_at: null, last_error_code: null, last_error_at: null, object: "core.model_configuration", harness: "codex", model: "test-model", harness_config: {}, model_provider: { protocol: "responses", base_url: "https://model.example/v1", api_key_configured: true }, updated_at: "2026-09-26T08:00:00Z" }; const harnesses = { object: "list", data: [ - { object: "core.harness", id: "claude_sdk", enabled: false, default: false, model_configuration: null, model_configuration_support: { protocols: ["anthropic"], accepts_harness_config: true } }, - { object: "core.harness", id: "codex", enabled: true, default: true, model_configuration: provider, model_configuration_support: { protocols: ["responses"], accepts_harness_config: true } }, + { object: "core.harness", id: "claude_sdk", enabled: false, default: false, model_configuration: null, model_configuration_support: { protocols: ["anthropic"], accepts_harness_config: true, accepts_max_output_tokens: true } }, + { object: "core.harness", id: "codex", enabled: true, default: true, model_configuration: provider, model_configuration_support: { protocols: ["responses"], accepts_harness_config: true, accepts_max_output_tokens: false } }, ] }; expect(await clientWith(harnesses).client.listHarnesses()).toEqual(harnesses); expect(await clientWith(provider).client.retrieveHarnessModelConfiguration("codex")).toEqual(provider); diff --git a/packages/agents-client/src/admin-projection.ts b/packages/agents-client/src/admin-projection.ts index 9e6e3cae3..714bd6fac 100644 --- a/packages/agents-client/src/admin-projection.ts +++ b/packages/agents-client/src/admin-projection.ts @@ -226,10 +226,11 @@ function projectModelConfigurationSupport(value: unknown): CoreHarness["model_co const protocols = support.protocols; if (!Array.isArray(protocols) || protocols.length === 0 || !protocols.every((protocol) => isOneOf(modelProviderProtocols, protocol)) || new Set(protocols).size !== protocols.length || - typeof support.accepts_harness_config !== "boolean") return invalidAdminResponse(); + typeof support.accepts_harness_config !== "boolean" || typeof support.accepts_max_output_tokens !== "boolean") return invalidAdminResponse(); return { protocols: [...protocols], accepts_harness_config: support.accepts_harness_config, + accepts_max_output_tokens: support.accepts_max_output_tokens, }; } export function projectCoreHarnessList(value: unknown): { object: "list"; data: CoreHarness[] } { diff --git a/packages/agents-client/src/generated/core-api.ts b/packages/agents-client/src/generated/core-api.ts index b254e58f7..bf70a525e 100644 --- a/packages/agents-client/src/generated/core-api.ts +++ b/packages/agents-client/src/generated/core-api.ts @@ -405,9 +405,10 @@ export const modelConfigurationInputFields = ["harness_config", "model", "model_ export const modelConfigurationInputRequired = ["model", "model_provider"] as const; export interface ModelConfigurationSupport { accepts_harness_config: boolean; + accepts_max_output_tokens: boolean; protocols: string[]; } -export const modelConfigurationSupportFields = ["accepts_harness_config", "protocols"] as const; +export const modelConfigurationSupportFields = ["accepts_harness_config", "accepts_max_output_tokens", "protocols"] as const; export interface Node { active: number; available_disk_bytes: number | null; diff --git a/packages/agents-client/src/provider-observations.test.ts b/packages/agents-client/src/provider-observations.test.ts index bd5ef988a..f0adafd2b 100644 --- a/packages/agents-client/src/provider-observations.test.ts +++ b/packages/agents-client/src/provider-observations.test.ts @@ -16,11 +16,11 @@ describe("deployment default observations", () => { }); describe("adapter model configuration support", () => { - const support = { protocols: ["responses"], accepts_harness_config: true }; + const support = { protocols: ["responses"], accepts_harness_config: true, accepts_max_output_tokens: false }; const harness = { object: "core.harness", id: "codex", enabled: false, default: false, model_configuration: null, model_configuration_support: support }; it("preserves build declarations and native default order without inferring readiness", () => { const claude = { ...harness, id: "claude_sdk", model_configuration_support: { ...support, protocols: ["anthropic"] } }; - const minimax = { ...harness, id: "mcode", model_configuration_support: { protocols: ["anthropic", "responses", "chat_completions"], accepts_harness_config: false } }; + const minimax = { ...harness, id: "mcode", model_configuration_support: { protocols: ["anthropic", "responses", "chat_completions"], accepts_harness_config: false, accepts_max_output_tokens: true } }; const value = { object: "list", data: [harness, claude, minimax] }; expect(projectCoreHarnessList(value)).toEqual(value); const projected = projectCoreHarnessList(value).data[2]!.model_configuration_support.protocols; diff --git a/packages/claude-sdk-adapter/src/workspace.ts b/packages/claude-sdk-adapter/src/workspace.ts index 032234def..847774070 100644 --- a/packages/claude-sdk-adapter/src/workspace.ts +++ b/packages/claude-sdk-adapter/src/workspace.ts @@ -20,7 +20,8 @@ export type Workspace = { const environmentNames = new Set([ "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL", "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL", - "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", + "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS", "CLAUDE_CODE_MAX_CONTEXT_TOKENS", "CLAUDE_CODE_MAX_OUTPUT_TOKENS", + "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", ]); const invalidPath = /[\x00-\x1f\x7f]/; diff --git a/services/core/internal/api/harness_model_providers.go b/services/core/internal/api/harness_model_providers.go index cf2e68af8..f49088f97 100644 --- a/services/core/internal/api/harness_model_providers.go +++ b/services/core/internal/api/harness_model_providers.go @@ -96,7 +96,7 @@ func (h *Handler) listHarnesses(w http.ResponseWriter, r *http.Request) { list := CoreHarnessList{Object: "list", Data: []CoreHarness{}} for _, kind := range builtin.Kinds() { declaration, _ := builtin.Registry().Lookup(kind) - support := v1.ModelConfigurationSupport{Protocols: []string{}, AcceptsHarnessConfig: declaration.AcceptsHarnessConfig()} + support := v1.ModelConfigurationSupport{Protocols: []string{}, AcceptsHarnessConfig: declaration.AcceptsHarnessConfig(), AcceptsMaxOutputTokens: declaration.MaxOutputTokens} for _, provider := range declaration.Providers { support.Protocols = append(support.Protocols, provider.Protocol) } From 3aeddc81b626b27f21fb68c6deedefb31377f900 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Mon, 19 Oct 2026 18:10:27 +0000 Subject: [PATCH 2/5] Show Codex without a native output limit --- apps/web/e2e/fixture-console.mjs | 4 ++-- apps/web/e2e/system.spec.ts | 21 +++++++++++-------- .../v1/model_provider_error_test.go | 2 +- .../core/internal/api/saved_provider_test.go | 4 ++-- .../execution/model_execution_test.go | 5 ++++- 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/apps/web/e2e/fixture-console.mjs b/apps/web/e2e/fixture-console.mjs index b81283af2..811f0d9e1 100644 --- a/apps/web/e2e/fixture-console.mjs +++ b/apps/web/e2e/fixture-console.mjs @@ -540,7 +540,7 @@ function providerProblem(harness, input) { if (!httpsBase(input.base_url)) return "model provider requires an HTTPS base_url without credentials, query or fragment"; if (!["anthropic", "responses", "chat_completions"].includes(input.protocol)) return "unsupported model provider protocol"; if (!input.api_key.trim() || Buffer.byteLength(input.api_key) > 16384 || /[\0\r\n]/.test(input.api_key)) return "invalid model provider API key"; - if ((input.max_output_tokens ?? 0) > (input.context_window ?? 0)) return "invalid model token limits"; + if (input.context_window && (input.max_output_tokens ?? 0) > input.context_window) return "invalid model token limits"; return null; } @@ -553,7 +553,7 @@ function providerProblem(harness, input) { * DELETE is 204 and safe to repeat. A disabled harness may still be configured. */ async function harnessRoute(request, response, path) { - const support = (id) => ({ protocols: id === "codex" ? ["responses"] : id === "claude_sdk" ? ["anthropic"] : ["anthropic", "responses", "chat_completions"], accepts_harness_config: id === "claude_sdk" }); + const support = (id) => ({ protocols: id === "codex" ? ["responses"] : id === "claude_sdk" ? ["anthropic"] : ["anthropic", "responses", "chat_completions"], accepts_harness_config: id === "claude_sdk", accepts_max_output_tokens: id !== "codex" }); const view = (id) => ({ model_configuration_support: support(id), object: "core.harness", id, enabled: state.harnesses[id].enabled, default: state.harnesses[id].default, model_configuration: state.harnesses[id].provider }); if (path === "/harnesses" && request.method === "GET") return send(response, 200, { object: "list", data: Object.keys(state.harnesses).map(view) }); const match = path.match(HARNESS_PROVIDER); diff --git a/apps/web/e2e/system.spec.ts b/apps/web/e2e/system.spec.ts index 5a79065d3..cf4277fac 100644 --- a/apps/web/e2e/system.spec.ts +++ b/apps/web/e2e/system.spec.ts @@ -41,21 +41,15 @@ test("sets, replaces and clears a harness's default model configuration, and kee // Codex accepts no native Harness configuration. await set.getByText("Advanced settings", { exact: true }).click(); await expect(set.getByLabel("Harness configuration (JSON)")).toHaveCount(0); - // Limits are Core's 32-bit whole numbers, and max output fits a set context window. - await set.getByLabel("Max output tokens").fill("32000"); - await expect(set.getByLabel("Context window", { exact: true })).not.toHaveAttribute("aria-invalid", "true"); - await set.getByLabel("Context window", { exact: true }).fill("1000"); - await expect(set.getByText("Set a context window at least this large.")).toBeVisible(); - await expect(set.getByLabel("Context window", { exact: true })).toHaveAttribute("aria-invalid", "true"); - await expect(set.getByLabel("Context window", { exact: true })).toHaveAccessibleDescription(/Set a context window at least this large/); - await expect(set.getByLabel("Max output tokens", { exact: true })).not.toHaveAttribute("aria-invalid", "true"); + // Codex has no native output limit; limits are Core's 32-bit whole numbers. + await expect(set.getByLabel("Max output tokens")).toHaveCount(0); await set.getByLabel("Context window").fill("2147483648"); await expect(set.getByText("Enter a whole number up to 2147483647.")).toBeVisible(); await expect(set.getByRole("button", { name: "Save" })).toBeDisabled(); await set.getByLabel("Context window").fill("200000"); await set.getByRole("button", { name: "Save" }).click(); await expect(set).toBeHidden(); - for (const text of ["Ready", "OpenAI Responses", "https://model.example/v1", "Configured", "200,000", "32,000"]) await expect(codex).toContainText(text); + for (const text of ["Ready", "OpenAI Responses", "https://model.example/v1", "Configured", "200,000"]) await expect(codex).toContainText(text); expect(await page.content()).not.toContain(KEY); // Replacing starts from the saved fields but never from the key; closing the form forgets a typed key. @@ -131,6 +125,15 @@ test("sets, replaces and clears a harness's default model configuration, and kee await limits.getByLabel("Base URL").fill("https://model.example/anthropic"); await limits.getByLabel("API key").fill(KEY); await limits.getByLabel("Default model ID").fill("fixture-model"); + // Max output alone is valid and must fit a set context window. + await limits.getByText("Advanced settings", { exact: true }).click(); + await limits.getByLabel("Max output tokens").fill("32000"); + await expect(limits.getByLabel("Context window", { exact: true })).not.toHaveAttribute("aria-invalid", "true"); + await limits.getByLabel("Context window", { exact: true }).fill("1000"); + await expect(limits.getByText("Set a context window at least this large.")).toBeVisible(); + await expect(limits.getByLabel("Context window", { exact: true })).toHaveAccessibleDescription(/Set a context window at least this large/); + await expect(limits.getByLabel("Max output tokens", { exact: true })).not.toHaveAttribute("aria-invalid", "true"); + await limits.getByLabel("Context window", { exact: true }).fill(""); await expect(limits.getByRole("button", { name: "Save" })).toBeEnabled(); await limits.getByRole("button", { name: "Cancel" }).click(); await expect(mcode).toContainText("Not set"); diff --git a/contracts/agents-api/v1/model_provider_error_test.go b/contracts/agents-api/v1/model_provider_error_test.go index 0b264d6e4..3952c6f30 100644 --- a/contracts/agents-api/v1/model_provider_error_test.go +++ b/contracts/agents-api/v1/model_provider_error_test.go @@ -39,7 +39,7 @@ func TestModelProviderErrorMessagesAndPrecedence(t *testing.T) { } outputOnly := valid outputOnly.MaxOutputTokens = 32000 - if err := outputOnly.ValidateHarness("codex"); err != nil { + if err := outputOnly.ValidateHarness("mcode"); err != nil { t.Fatal(err) // An omitted context window keeps the native default. } if err := valid.ValidateHarness("codex"); err != nil { diff --git a/services/core/internal/api/saved_provider_test.go b/services/core/internal/api/saved_provider_test.go index 9c8a9bc0a..bce769af2 100644 --- a/services/core/internal/api/saved_provider_test.go +++ b/services/core/internal/api/saved_provider_test.go @@ -12,7 +12,7 @@ import ( "github.com/google/uuid" ) -const savedProviderFixture = `{"protocol":"responses","base_url":"https://example.test/v1","api_key":"saved-provider-secret","context_window":100000,"max_output_tokens":8000}` +const savedProviderFixture = `{"protocol":"responses","base_url":"https://example.test/v1","api_key":"saved-provider-secret","context_window":100000}` type savedProviderStore struct { saved agents.Agent @@ -139,7 +139,7 @@ func TestSavedProviderInvalidInput(t *testing.T) { `{"model_provider":{"protocol":"responses","base_url":"https://example.test","api_key":"saved-provider-secret","API_KEY":"secret"}}`, `{"model_provider":{"protocol":"responses","base_url":"https://example.test","api_key":"saved-provider-secret","api_key":"other"}}`, `{"harness":"claude_sdk","model_provider":` + strings.Replace(savedProviderFixture, `"responses"`, `"unknown"`, 1) + `}`, - `{"model_provider":` + strings.Replace(savedProviderFixture, `"max_output_tokens":8000`, `"max_output_tokens":100001`, 1) + `}`, + `{"model_provider":` + strings.Replace(savedProviderFixture, `"context_window":100000`, `"context_window":-1`, 1) + `}`, } { for _, path := range []string{"/v1/agents", "/v1/agents/" + uuid.NewString()} { h, s := validationHandler(t) diff --git a/services/core/internal/execution/model_execution_test.go b/services/core/internal/execution/model_execution_test.go index 82ebeb18f..dd1a0aa87 100644 --- a/services/core/internal/execution/model_execution_test.go +++ b/services/core/internal/execution/model_execution_test.go @@ -35,7 +35,10 @@ func TestSessionModelOptionsPreserveUpstreamBundleForEveryHarness(t *testing.T) for _, engine := range []string{"codex", "claude_sdk", "mcode"} { for _, protocol := range []string{"anthropic", "responses", "chat_completions"} { t.Run(engine+"/"+protocol, func(t *testing.T) { - provider := &v1.ModelProviderInput{Protocol: modelprovider.Protocol(protocol), BaseURL: "https://example.com/v1", APIKey: "private-key", ContextWindow: 200000, MaxOutputTokens: 8000} + provider := &v1.ModelProviderInput{Protocol: modelprovider.Protocol(protocol), BaseURL: "https://example.com/v1", APIKey: "private-key", ContextWindow: 200000} + if engine != "codex" { + provider.MaxOutputTokens = 8000 // Codex has no native output limit. + } got, err := resolvedSessionModelOptions(provider, engine) native := engine == "mcode" || engine == "codex" && protocol == "responses" || engine == "claude_sdk" && protocol == "anthropic" if !native { From aa7f58b7b2380a9c36c0b2921530fdd042da15a5 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Mon, 19 Oct 2026 18:23:13 +0000 Subject: [PATCH 3/5] Refresh the model execution translation hash --- contracts/agents-api/zh/model-execution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/agents-api/zh/model-execution.md b/contracts/agents-api/zh/model-execution.md index c496c0797..3436cd82d 100644 --- a/contracts/agents-api/zh/model-execution.md +++ b/contracts/agents-api/zh/model-execution.md @@ -1,7 +1,7 @@ --- title: "模型执行" source: contracts/agents-api/model-execution.md -source_hash: 5ebda69322721893a97cb6814ea72df3ca8c02d1fc9b59724e5392f8e2583db9 +source_hash: 1ceb0a7799a57aa1e53b6354b0c4c6acd1002e328430c7a98dd0713f9f7a334b --- 每个 Session 都运行一个 Harness,并使用一个模型提供商。Core 通过三个固定版本上游协议未定义的 Core 扩展来选择它们:`x_agents_core.harness` 选择 Harness,`x_agents_core.model_provider` 提供端点和密钥,`x_agents_core.harness_config` 携带原生模型参数。Core 没有提供商目录、模型别名解析或产品权限模型;除 Session 和已保存 Agent 配置包外,唯一存储的配置包是每个 Harness 的一个 [deployment default](#deployment-defaults)。本文档定义 Harness—模型提供商协议:[`internal/modelprovider/config.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/modelprovider/config.go) 负责验证冻结的提供商连接,每个 Harness 则通过 [`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go) 声明其协议和原生参数。 From 8087d8d1107b5e7e2af97e67ebb56641a9c7be39 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Mon, 19 Oct 2026 18:35:20 +0000 Subject: [PATCH 4/5] Read only the protocols column in the website check --- website/tests/harnesses.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/tests/harnesses.test.mjs b/website/tests/harnesses.test.mjs index 94f16a39c..41cca7a25 100644 --- a/website/tests/harnesses.test.mjs +++ b/website/tests/harnesses.test.mjs @@ -17,7 +17,7 @@ test('harness protocols match Model execution, default first', () => { const doc = readFileSync(resolve(repoRoot, 'contracts/agents-api/model-execution.md'), 'utf8') const table = doc.split('| Harness | Supported protocols, default first |')[1].split('\n\n')[0] const rows = Object.fromEntries( - [...table.matchAll(/^\| ([^|]+) \| (.+) \|$/gm)] + [...table.matchAll(/^\| ([^|]+) \| ([^|]+) \|/gm)] .filter(([, name]) => !name.startsWith('---')) .map(([, name, protocols]) => [name.trim(), [...protocols.matchAll(/`([a-z_]+)`/g)].map((m) => m[1])]), ) From 68774d21b4b09558d086a89854a2b8bfb6d03f0f Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Mon, 19 Oct 2026 19:10:07 +0000 Subject: [PATCH 5/5] Accept output-only limits in the client and keep daemon limits out of Claude --- apps/daemon/internal/agent/claudesdk/workspace.go | 3 ++- .../agents-client/src/execution-configuration-projection.ts | 2 +- packages/agents-client/src/execution-configuration.test.ts | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/daemon/internal/agent/claudesdk/workspace.go b/apps/daemon/internal/agent/claudesdk/workspace.go index f8fda8e1b..640e5cbc2 100644 --- a/apps/daemon/internal/agent/claudesdk/workspace.go +++ b/apps/daemon/internal/agent/claudesdk/workspace.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "strings" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" @@ -102,7 +103,7 @@ func workspaceEnvironment(config Config) (*workspaceProfile, []string, error) { env := []string{} for _, entry := range os.Environ() { name, _, _ := strings.Cut(entry, "=") - if name != "ANTHROPIC_API_KEY" && name != "ANTHROPIC_AUTH_TOKEN" && name != "CLAUDE_CODE_OAUTH_TOKEN" && name != "CLAUDE_CONFIG_DIR" && name != "HOME" && name != "TMPDIR" { + if !slices.Contains([]string{"ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN", "CLAUDE_CODE_MAX_CONTEXT_TOKENS", "CLAUDE_CODE_MAX_OUTPUT_TOKENS", "CLAUDE_CONFIG_DIR", "HOME", "TMPDIR"}, name) { env = append(env, entry) } } diff --git a/packages/agents-client/src/execution-configuration-projection.ts b/packages/agents-client/src/execution-configuration-projection.ts index e0a1506cc..7a5cf65e4 100644 --- a/packages/agents-client/src/execution-configuration-projection.ts +++ b/packages/agents-client/src/execution-configuration-projection.ts @@ -40,7 +40,7 @@ export function safeProvider(value: unknown, invalid: Invalid): ModelProviderVie typeof value.base_url !== "string" || typeof value.api_key_configured !== "boolean" || (value.context_window !== undefined && !isNonnegativeInteger(value.context_window)) || (value.max_output_tokens !== undefined && !isNonnegativeInteger(value.max_output_tokens)) || - Number(value.max_output_tokens ?? 0) > Number(value.context_window ?? 0)) return invalid(); + (Number(value.context_window ?? 0) > 0 && Number(value.max_output_tokens ?? 0) > Number(value.context_window))) return invalid(); if (!safeBaseURL(value.base_url)) return invalid(); return { protocol: value.protocol as ModelProviderView["protocol"], base_url: value.base_url, api_key_configured: value.api_key_configured, diff --git a/packages/agents-client/src/execution-configuration.test.ts b/packages/agents-client/src/execution-configuration.test.ts index 4a2f36dcd..89db539ed 100644 --- a/packages/agents-client/src/execution-configuration.test.ts +++ b/packages/agents-client/src/execution-configuration.test.ts @@ -35,6 +35,8 @@ describe("frozen execution configuration", () => { value.model_provider.configuration!.context_window = 200000; value.model_provider.configuration!.max_output_tokens = 8000; expect(await clientReturning(value).retrieveSessionExecutionConfiguration(projectId, id)).toEqual(value); + delete value.model_provider.configuration!.context_window; + expect(await clientReturning(value).retrieveSessionExecutionConfiguration(projectId, id)).toEqual(value); } }); it.each([