From 5923b70ba37c8dc9cf054a84a7b7522e41742009 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Mon, 19 Oct 2026 08:49:23 +0000 Subject: [PATCH 1/3] Let model providers omit token limits --- .../internal/agent/mcode/model_provider.go | 17 +++++++-- .../agent/mcode/model_provider_test.go | 38 ++++++++++++++++++- apps/web/DESIGN.md | 2 +- apps/web/e2e/fixture-console.mjs | 2 +- .../features/overview/getting-started.test.ts | 2 +- .../features/system/ModelProviderDialog.tsx | 8 ++-- apps/web/src/i18n/locales/en/core-errors.ts | 2 +- apps/web/src/i18n/locales/en/system.ts | 2 - .../web/src/i18n/locales/zh-CN/core-errors.ts | 2 +- apps/web/src/i18n/locales/zh-CN/system.ts | 2 - contracts/agents-api/core.openapi.yaml | 3 -- contracts/agents-api/model-execution.md | 6 +-- .../agents-api/v1/model_configuration.go | 1 - contracts/agents-api/v1/model_execution.go | 7 ---- .../agents-api/v1/model_execution_test.go | 17 ++------- .../v1/model_provider_error_test.go | 2 - .../agents-api/v1/saved_core_extension.go | 2 +- .../v1/saved_core_extension_test.go | 6 +-- contracts/agents-api/zh/model-execution.md | 8 ++-- example/parsar/README.md | 4 +- internal/harnessconfig/configuration_test.go | 8 ++-- internal/harnessconfig/harness.go | 19 +--------- internal/harnessconfig/mcode/declaration.go | 6 +-- internal/harnessconfig/provider.go | 2 +- .../agents-client/src/admin-client.test.ts | 4 +- .../agents-client/src/admin-projection.ts | 4 +- .../agents-client/src/generated/core-api.ts | 3 +- .../src/provider-observations.test.ts | 8 ++-- scripts/acceptance/acceptance.py | 3 -- scripts/acceptance/harness_catalog.py | 10 ++--- scripts/acceptance/test_acceptance.py | 21 ++-------- scripts/generate-harness-catalog.py | 8 ++-- scripts/generate-harness-catalog.test.py | 7 ++-- scripts/harness-catalog/main.go | 16 +++----- scripts/harness-catalog/main_test.go | 8 ++-- services/core/deploy/mcode/README.md | 2 +- .../core_model_provider_validation_test.go | 2 - .../internal/api/harness_model_providers.go | 1 - .../theme/components/ComposeLab.vue | 4 -- 39 files changed, 118 insertions(+), 151 deletions(-) diff --git a/apps/daemon/internal/agent/mcode/model_provider.go b/apps/daemon/internal/agent/mcode/model_provider.go index 852fe3088..3e85d701d 100644 --- a/apps/daemon/internal/agent/mcode/model_provider.go +++ b/apps/daemon/internal/agent/mcode/model_provider.go @@ -21,12 +21,21 @@ func modelProviderConfig(raw any, model string) (map[string]any, error) { case modelprovider.Responses: api = "openai-responses" } + native := map[string]any{"name": model, "tool_call": true} + // Absent limits are omitted so the native defaults apply. + limit := map[string]any{} + if provider.ContextWindow > 0 { + limit["context"] = provider.ContextWindow + } + if provider.MaxOutputTokens > 0 { + limit["output"] = provider.MaxOutputTokens + } + if len(limit) > 0 { + native["limit"] = limit + } return map[string]any{ "name": "Configured provider", "kind": "custom", "enabled": true, "api": api, "options": map[string]any{"baseURL": provider.BaseURL, "apiKey": provider.APIKey}, - "models": map[string]any{model: map[string]any{ - "name": model, "tool_call": true, - "limit": map[string]any{"context": provider.ContextWindow, "output": provider.MaxOutputTokens}, - }}, + "models": map[string]any{model: native}, }, nil } diff --git a/apps/daemon/internal/agent/mcode/model_provider_test.go b/apps/daemon/internal/agent/mcode/model_provider_test.go index 72089c3db..b466be76d 100644 --- a/apps/daemon/internal/agent/mcode/model_provider_test.go +++ b/apps/daemon/internal/agent/mcode/model_provider_test.go @@ -43,6 +43,43 @@ func TestOptionsModelProviderProtocols(t *testing.T) { } } +func TestOptionsOmitAbsentTokenLimits(t *testing.T) { + for _, tc := range []struct { + name string + unset []string + want any + }{ + {"none", []string{"context_window", "max_output_tokens"}, nil}, + {"context only", []string{"max_output_tokens"}, map[string]any{"context": float64(64000)}}, + } { + t.Run(tc.name, func(t *testing.T) { + req := testRequest(t) + for _, field := range tc.unset { + delete(req.AgentOptions["model_provider"].(map[string]any), field) + } + opts, err := prepareOptions(testStateRoot(t), req, nil) + if err != nil { + t.Fatal(err) + } + body, err := os.ReadFile(filepath.Join(opts.DataDir, "config.yaml")) + if err != nil { + t.Fatal(err) + } + var config struct { + CustomProvider map[string]struct { + Models map[string]map[string]any `json:"models"` + } `json:"custom_provider"` + } + if err := json.Unmarshal(body, &config); err != nil { + t.Fatal(err) + } + if got := config.CustomProvider["oac"].Models[opts.Model]["limit"]; !reflect.DeepEqual(got, tc.want) { + t.Fatalf("limit = %v, want %v", got, tc.want) + } + }) + } +} + func TestOptionsRejectInvalidProvider(t *testing.T) { for _, tc := range []struct { name, field string @@ -53,7 +90,6 @@ func TestOptionsRejectInvalidProvider(t *testing.T) { {"remote HTTP", "base_url", "http://provider.example/v1"}, {"empty key", "api_key", ""}, {"missing context", "context_window", 0}, - {"missing output", "max_output_tokens", 0}, {"excess output", "max_output_tokens", 64001}, {"native options", "options", map[string]any{"apiKey": "fixture-key"}}, } { diff --git a/apps/web/DESIGN.md b/apps/web/DESIGN.md index 5966de38f..b3d4c5f74 100644 --- a/apps/web/DESIGN.md +++ b/apps/web/DESIGN.md @@ -434,7 +434,7 @@ Sandbox and scheduling contains sandbox setup or Deployment provider (Provider, Workspace storage shows the selected configuration's adapter and tenant capacity (in bytes units, or No tenant capacity limit) under Storage configuration, with Select new configuration in the section header and Edit tenant capacity on the capacity row. The configuration ID and parameters JSON sit in a closed Configuration details disclosure at the end of the card. Select new configuration accepts an adapter identifier, configuration ID, parameters JSON object and optional write-only credential JSON object. Edit tenant capacity keeps the current configuration identity. Explain adapter preparation and existing-workspace behavior behind help tips, with a link to the owning configuration document; keep required form labels and validation visible. Never prefill credentials or persist their drafts across navigation. A failed read is not an unconfigured filesystem, and an uncertain write is not a safe automatic retry. -Models and credentials stacks one default-model card per Harness. Its header carries the Harness name, its readiness (Ready in green; Model provider not set in amber, with Set as the primary button; Disabled in neutral, with a View startup settings text action), a Default pill, a help tip and the Set, Replace and Clear actions; rows hold model ID, provider protocol, base URL, key presence, token limits when set and update time, and a foot holds Usage details. Set and Replace open one form dialog. The adapter declaration supplies supported protocols, native JSON support and required limits without harness-specific branches. Native settings use an optional JSON object editor with formatting and inline syntax errors. Changing model identity clears native JSON; replacing only the required write-only API key does not. Keys are never prefilled and are forgotten when the form closes. Core rejections remain visible in the form; Clear uses ConfirmDialog. Usage details opens Core's observations separately. +Models and credentials stacks one default-model card per Harness. Its header carries the Harness name, its readiness (Ready in green; Model provider not set in amber, with Set as the primary button; Disabled in neutral, with a View startup settings text action), a Default pill, a help tip and the Set, Replace and Clear actions; rows hold model ID, provider protocol, base URL, key presence, token limits when set and update time, and a foot holds Usage details. Set and Replace open one form dialog. The adapter declaration supplies supported protocols and native JSON support without harness-specific branches. Native settings use an optional JSON object editor with formatting and inline syntax errors. Changing model identity clears native JSON; replacing only the required write-only API key does not. Keys are never prefilled and are forgotten when the form closes. Core rejections remain visible in the form; Clear uses ConfirmDialog. Usage details opens Core's observations separately. Service and deployment shows installation addresses, an Installation details disclosure, closed by default, with the installation ID and source commit, existing Domain and HTTPS actions, and the startup settings Core loaded. Process settings remain read-only here; their source stays the installation configuration. Each row's label is the setting's environment variable in code style (OAC_LOG_LEVEL), as Core reports it, wrapping after an underscore so a phone keeps all three columns; values show as is, with arrays joined, On or Off for booleans and None for an empty list. How to change a setting in the section header links to the configuration guide. Sensitive settings show only Configured or Not set. Default uses a neutral pill. diff --git a/apps/web/e2e/fixture-console.mjs b/apps/web/e2e/fixture-console.mjs index 7606bac36..874a8b997 100644 --- a/apps/web/e2e/fixture-console.mjs +++ b/apps/web/e2e/fixture-console.mjs @@ -554,7 +554,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", token_limits_required: id === "mcode" }); + const support = (id) => ({ protocols: id === "codex" ? ["responses"] : id === "claude_sdk" ? ["anthropic"] : ["anthropic", "responses", "chat_completions"], accepts_harness_config: id === "claude_sdk" }); 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/src/features/overview/getting-started.test.ts b/apps/web/src/features/overview/getting-started.test.ts index 97249ca59..76ae44806 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, token_limits_required: false }, 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 }, 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 9a145278f..2f97f6432 100644 --- a/apps/web/src/features/system/ModelProviderDialog.tsx +++ b/apps/web/src/features/system/ModelProviderDialog.tsx @@ -56,7 +56,6 @@ export function ModelProviderDialog({ harness, onClose, onSaved, onReread }: { const configuration = harness?.model_configuration ?? null; const current = configuration?.model_provider ?? null; const support = harness?.model_configuration_support; - const limitsRequired = support?.token_limits_required ?? false; const protocolOptions = (support?.protocols ?? []).map((value) => ({ value, label: protocolNames[value] })); const [protocol, setProtocol] = useState(current?.protocol ?? support?.protocols[0] ?? ""); const [baseUrl, setBaseUrl] = useState(current?.base_url ?? ""); @@ -64,7 +63,7 @@ export function ModelProviderDialog({ harness, onClose, onSaved, onReread }: { const [model, setModel] = useState(configuration?.model ?? ""); const [configText, setConfigText] = useState(JSON.stringify(configuration?.harness_config ?? {}, null, 2)); const [configTouched, setConfigTouched] = useState(false); - const [advancedOpen, setAdvancedOpen] = useState(Boolean(configuration && (Object.keys(configuration.harness_config ?? {}).length || current?.context_window || current?.max_output_tokens)) || limitsRequired); + const [advancedOpen, setAdvancedOpen] = useState(Boolean(configuration && (Object.keys(configuration.harness_config ?? {}).length || current?.context_window || current?.max_output_tokens))); let nativeConfig: Record | null = null; try { const parsed: unknown = configText.trim() ? JSON.parse(configText) : {}; @@ -89,7 +88,7 @@ export function ModelProviderDialog({ harness, onClose, onSaved, onReread }: { const outputProblem = limitProblem(output); // The missing or undersized window is the field the administrator must fix. const contextProblem = limitProblem(context) ?? (!outputProblem && output.value !== undefined && output.value > (context.value ?? 0) ? t("models.form.needsContext") : null); - const ready = harness !== null && !busy && url !== "" && !urlProblem && apiKey.trim() !== "" && model.trim() !== "" && nativeConfig !== null && !configTooLarge && !protocolProblem && (!limitsRequired || Boolean(context.value && output.value)) && !contextProblem && !outputProblem; + const ready = harness !== null && !busy && url !== "" && !urlProblem && apiKey.trim() !== "" && model.trim() !== "" && nativeConfig !== null && !configTooLarge && !protocolProblem && !contextProblem && !outputProblem; function clearNativeConfig() { setConfigText("{}"); @@ -133,7 +132,7 @@ export function ModelProviderDialog({ harness, onClose, onSaved, onReread }: {
- {t(`models.form.${field}Help${limitsRequired ? "Required" : ""}`)} + {t(`models.form.${field}Help`)} { setValue(event.target.value); setRejection(null); setError(null); }} - aria-required={limitsRequired} aria-invalid={problem ? true : undefined} aria-describedby={`${inputId}-help${problem ? ` ${inputId}-problem` : ""}`} /> diff --git a/apps/web/src/i18n/locales/en/core-errors.ts b/apps/web/src/i18n/locales/en/core-errors.ts index e90ef877f..0b5e733d6 100644 --- a/apps/web/src/i18n/locales/en/core-errors.ts +++ b/apps/web/src/i18n/locales/en/core-errors.ts @@ -18,7 +18,7 @@ export const coreErrors = { "model_provider_base_url_invalid": "Use an HTTPS URL without credentials, query parameters or a fragment.", "model_provider_protocol_unsupported": "This protocol is not supported by the Harness.", "model_provider_api_key_invalid": "Enter a valid API key without control characters.", - "model_provider_token_limits_invalid": "Use valid token limits; output cannot exceed context, and required limits must be positive.", + "model_provider_token_limits_invalid": "Use valid token limits; output cannot exceed context.", "invalid_sandbox_configuration": "Check the sandbox resources and Runtime release.", "project_archived": "This Project is archived.", "project_exists": "A Project with this name already exists.", diff --git a/apps/web/src/i18n/locales/en/system.ts b/apps/web/src/i18n/locales/en/system.ts index 4fd419cf0..c5230056a 100644 --- a/apps/web/src/i18n/locales/en/system.ts +++ b/apps/web/src/i18n/locales/en/system.ts @@ -81,9 +81,7 @@ export const system = { baseUrlInvalid: "Enter an HTTPS URL with a hostname and no credentials, query or fragment, such as https://api.example.com/v1.", apiKeyHelp: "Required every time. Core encrypts it and never shows it again.", contextHelp: "The model's context window, in tokens. Optional.", - contextHelpRequired: "The model's context window, in tokens. This Harness requires it.", outputHelp: "The most tokens the model writes in one response. Optional; it needs a context window at least as large.", - outputHelpRequired: "The most tokens the model writes in one response. This Harness requires it, with a context window at least as large.", protocolHelp: "Choose a protocol supported by both this Harness and your model provider.", wholeNumber: "Enter a whole number.", tooLarge: "Enter a whole number up to 2147483647.", diff --git a/apps/web/src/i18n/locales/zh-CN/core-errors.ts b/apps/web/src/i18n/locales/zh-CN/core-errors.ts index 025305f7c..53ac68599 100644 --- a/apps/web/src/i18n/locales/zh-CN/core-errors.ts +++ b/apps/web/src/i18n/locales/zh-CN/core-errors.ts @@ -17,7 +17,7 @@ export const coreErrors = { "model_provider_base_url_invalid": "请使用 HTTPS 地址,不要包含凭据、查询参数或片段。", "model_provider_protocol_unsupported": "此 Harness 不支持该协议。", "model_provider_api_key_invalid": "请输入有效的 API 密钥,不要包含控制字符。", - "model_provider_token_limits_invalid": "请填写有效的 token 限制;输出不能超过上下文,必填限制必须大于零。", + "model_provider_token_limits_invalid": "请填写有效的 token 限制;输出不能超过上下文。", "invalid_sandbox_configuration": "请检查沙箱资源和 Runtime 版本。", "project_archived": "此项目已归档。", "project_exists": "此项目名称已被使用。", diff --git a/apps/web/src/i18n/locales/zh-CN/system.ts b/apps/web/src/i18n/locales/zh-CN/system.ts index 19c5c820d..7e63fcb06 100644 --- a/apps/web/src/i18n/locales/zh-CN/system.ts +++ b/apps/web/src/i18n/locales/zh-CN/system.ts @@ -83,9 +83,7 @@ export const system: TranslationShape = { baseUrlInvalid: "请输入包含主机名、不含账号密码、查询参数或片段的 HTTPS URL,例如 https://api.example.com/v1。", apiKeyHelp: "每次都必须填写。Core 会加密保存,之后不再显示。", contextHelp: "模型的上下文窗口,单位为 token。可选。", - contextHelpRequired: "模型的上下文窗口,单位为 token。此 Harness 必须填写。", outputHelp: "模型单次回复最多输出的 token 数。可选;填写时,上下文窗口必须不小于它。", - outputHelpRequired: "模型单次回复最多输出的 token 数。此 Harness 必须填写,且上下文窗口不能小于它。", protocolHelp: "选择此 Harness 和模型提供商都支持的协议。", wholeNumber: "请输入整数。", tooLarge: "请输入不超过 2147483647 的整数。", diff --git a/contracts/agents-api/core.openapi.yaml b/contracts/agents-api/core.openapi.yaml index 9ae446bea..1c360662d 100644 --- a/contracts/agents-api/core.openapi.yaml +++ b/contracts/agents-api/core.openapi.yaml @@ -2541,12 +2541,9 @@ definitions: items: type: string type: array - token_limits_required: - type: boolean required: - accepts_harness_config - protocols - - token_limits_required type: object v1.ModelProviderInput: properties: diff --git a/contracts/agents-api/model-execution.md b/contracts/agents-api/model-execution.md index ab06d65e3..8679d9a21 100644 --- a/contracts/agents-api/model-execution.md +++ b/contracts/agents-api/model-execution.md @@ -23,7 +23,7 @@ Supported protocols by Harness: | Claude Code | `anthropic` | | MiniMax Code | `anthropic`, `responses`, `chat_completions` | -MiniMax Code requires positive context and output limits. 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. +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. ## Harness selection @@ -109,7 +109,7 @@ At dispatch, Core sends the snapshot as one confidential provider bundle over th | --- | --- | --- | | Codex | Empty object only | Reasoning effort belongs to the public `reasoning.effort` | | Claude Code | `thinking`: the SDK's `adaptive`, `enabled` or `disabled` object | SDK `Options.thinking`; the public `reasoning.effort` becomes `Options.effort` | -| MiniMax Code | Empty object only | Provider token limits remain required | +| MiniMax Code | Empty object only | No native model parameters | Claude `thinking` accepts `display` (`summarized` or `omitted`) for `adaptive` and `enabled`, and a positive integer `budgetTokens` only for `enabled`; `maxThinkingTokens` is rejected. These are native settings, not a shared reasoning vocabulary; model availability and provider support are the Harness's responsibility. @@ -132,7 +132,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, `accepts_harness_config` says whether native parameters are accepted and `token_limits_required` whether provider token limits are required. 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, and `accepts_harness_config` says whether native parameters are accepted. 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 001285756..7aeb9926a 100644 --- a/contracts/agents-api/v1/model_configuration.go +++ b/contracts/agents-api/v1/model_configuration.go @@ -59,7 +59,6 @@ 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"` - TokenLimitsRequired bool `json:"token_limits_required" 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 66b75e64b..e135c855e 100644 --- a/contracts/agents-api/v1/model_execution.go +++ b/contracts/agents-api/v1/model_execution.go @@ -75,12 +75,5 @@ 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.Validate(string(p.Protocol), p.ContextWindow, p.MaxOutputTokens); err != nil { - param := "max_output_tokens" - if p.ContextWindow <= 0 { - param = "context_window" - } - return &ModelProviderError{Code: "model_provider_token_limits_invalid", Param: param, 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 6b7532a47..fb6c5237e 100644 --- a/contracts/agents-api/v1/model_execution_test.go +++ b/contracts/agents-api/v1/model_execution_test.go @@ -17,7 +17,7 @@ func TestModelExecutionValidation(t *testing.T) { t.Fatalf("wrong native protocol admission: %v", err) } p.ContextWindow, p.MaxOutputTokens = 0, 0 - if err := p.ValidateHarness(harness); (err == nil) != (native && harness != "mcode") { + if err := p.ValidateHarness(harness); (err == nil) != native { t.Fatalf("incorrect optional token-limit admission: %v", err) } }) @@ -43,9 +43,6 @@ func TestModelExecutionValidation(t *testing.T) { t.Fatal("valid provider URL rejected", url, err) } } - if (&ModelProviderInput{Protocol: "anthropic", BaseURL: "https://example.com", APIKey: "secret"}).ValidateHarness("mcode") == nil { - t.Fatal("MiniMax Code accepted unknown model limits") - } } func TestProviderDeclarationsAgreeWithAdmission(t *testing.T) { @@ -56,7 +53,7 @@ func TestProviderDeclarationsAgreeWithAdmission(t *testing.T) { t.Fatal("registered harness has no declaration") } for _, protocol := range []string{"responses", "anthropic", "unknown"} { - provider, supported := declared.Provider(protocol) + _, 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 { t.Fatalf("protocol %q disagrees with validation", protocol) @@ -65,14 +62,8 @@ func TestProviderDeclarationsAgreeWithAdmission(t *testing.T) { continue } input.ContextWindow, input.MaxOutputTokens = 0, 0 - if (input.ValidateHarness(kind) != nil) != provider.RequiresTokenLimits { - t.Fatalf("token requirements disagree for %q", protocol) - } - if provider.RequiresTokenLimits { - input.ContextWindow = 100 - if input.ValidateHarness(kind) == nil { - t.Fatal("missing output limit accepted") - } + if input.ValidateHarness(kind) != nil { + t.Fatalf("provider without token limits rejected for %q", protocol) } } }) diff --git a/contracts/agents-api/v1/model_provider_error_test.go b/contracts/agents-api/v1/model_provider_error_test.go index bab331fdf..e783a8775 100644 --- a/contracts/agents-api/v1/model_provider_error_test.go +++ b/contracts/agents-api/v1/model_provider_error_test.go @@ -20,8 +20,6 @@ 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.MaxOutputTokens = 1 }}, - {"required context", "mcode", "model_provider_token_limits_invalid", "context_window", "selected harness requires positive model context_window and max_output_tokens", func(p *ModelProviderInput) { p.Protocol = "anthropic" }}, - {"required output", "mcode", "model_provider_token_limits_invalid", "max_output_tokens", "selected harness requires positive model context_window and max_output_tokens", func(p *ModelProviderInput) { p.Protocol = "anthropic"; p.ContextWindow = 10 }}, } { 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 6f2fc2733..af8969a25 100644 --- a/contracts/agents-api/v1/saved_core_extension.go +++ b/contracts/agents-api/v1/saved_core_extension.go @@ -79,5 +79,5 @@ func (p *ModelProviderView) ValidateHarnessWithRegistry(harness string, registry if p == nil { return errors.New("model_provider is required") } - return registry.Validate(harness, string(p.Protocol), p.ContextWindow, p.MaxOutputTokens) + return registry.ValidateProtocol(harness, string(p.Protocol)) } diff --git a/contracts/agents-api/v1/saved_core_extension_test.go b/contracts/agents-api/v1/saved_core_extension_test.go index e253998e1..5c30ace79 100644 --- a/contracts/agents-api/v1/saved_core_extension_test.go +++ b/contracts/agents-api/v1/saved_core_extension_test.go @@ -49,12 +49,12 @@ func TestSavedProviderExplicitHarnessCompatibility(t *testing.T) { } } for _, protocol := range []string{"anthropic", "responses", "chat_completions"} { - for _, limits := range [][2]int32{{0, 0}, {100, 0}, {0, 20}} { + for _, limits := range [][2]int32{{0, 0}, {100, 0}} { x := &SavedAgentCoreInput{Harness: "mcode", ModelProvider: &ModelProviderInput{ Protocol: modelprovider.Protocol(protocol), BaseURL: "https://example.test", APIKey: "fixture", ContextWindow: limits[0], MaxOutputTokens: limits[1], }} - if x.Validate() == nil || x.SafeView().ModelProvider.ValidateHarness("mcode") == nil { - t.Fatal("MiniMax limits must be complete for every upstream protocol") + if x.Validate() != nil || x.SafeView().ModelProvider.ValidateHarness("mcode") != nil { + t.Fatal("MiniMax Code must accept omitted token limits for every upstream protocol") } } } diff --git a/contracts/agents-api/zh/model-execution.md b/contracts/agents-api/zh/model-execution.md index 19214d49b..a79755733 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: d65e90e7ce794129a02a2091aadcee3919f8c4c22917d6558ed66933657896d4 +source_hash: 4beae38b5287fd8d50509d057e92062f6acf9011cf02170fd8f83ed5e2df5f4f --- 每个 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) 声明其协议和原生参数。 @@ -25,7 +25,7 @@ Harness 接收一个冻结的模型提供商配置包,其定义见 [`modelprov | Claude Code | `anthropic` | | MiniMax Code | `anthropic`、`responses`、`chat_completions` | -MiniMax Code 要求上下文限制和输出限制均为正数。Core 会在写入 Session 前验证解析后的组合。Core 和 Runtime 读取 `internal/harnessconfig` 中相同的有序 `protocols` 声明。不存在模型 API 代理、直通网关或跨协议转换,Harness 内部也不例外。不受支持的已保存配置和 Session 快照一旦使用便会失败;它们绝不会在何处被重写、创建别名或迁移。 +Core 会在写入 Session 前验证解析后的组合。Core 和 Runtime 读取 `internal/harnessconfig` 中相同的有序 `protocols` 声明。不存在模型 API 代理、直通网关或跨协议转换,Harness 内部也不例外。不受支持的已保存配置和 Session 快照一旦使用便会失败;它们绝不会在何处被重写、创建别名或迁移。 ## Harness 选择 {#harness-selection} @@ -111,7 +111,7 @@ Core 从同一个数据库快照读取 Agent 配置和加密配置包;显式 | --- | --- | --- | | Codex | 仅空对象 | 推理强度属于公开的 `reasoning.effort` | | Claude Code | `thinking`:SDK 的 `adaptive`、`enabled` 或 `disabled` 对象 | SDK `Options.thinking`;公开的 `reasoning.effort` 对应 `Options.effort` | -| MiniMax Code | 仅空对象 | 仍必须给出提供商 token 限制 | +| MiniMax Code | 仅空对象 | 没有原生模型参数 | 对于 `adaptive` 和 `enabled`,Claude `thinking` 接受 `display`(`summarized` 或 `omitted`);只有 `enabled` 接受正整数 `budgetTokens`;拒绝 `maxThinkingTokens`。这些是原生设置,而不是通用推理词汇表;模型可用性和提供商支持均由 Harness 负责。 @@ -134,7 +134,7 @@ PUT 要求提供 `model` 和完整的 `model_provider`;`harness_config` 默认 创建 Session 时,Core 会解密解析后 Harness 的默认值,并像处理其他配置包一样将其冻结在 Session 的加密快照中,因此更改或移除默认值绝不会影响现有 Session。execution-configuration 读取结果会显示冻结的安全视图,其来源为 `deployment`。提供商快照缺失或无效时会安全失败,并且不会回退到其他模型或提供商。 -`model_configuration_support` 派生自 Core 和 Runtime 共享的适配器声明:`protocols` 列出可选择的原生协议,并将默认项放在首位;`accepts_harness_config` 表示是否接受原生参数;`token_limits_required` 表示是否必须给出提供商 token 限制。它描述的是构建版本,而不是实时 Runtime 或远程模型。 +`model_configuration_support` 派生自 Core 和 Runtime 共享的适配器声明:`protocols` 列出可选择的原生协议,并将默认项放在首位;`accepts_harness_config` 表示是否接受原生参数。它描述的是构建版本,而不是实时 Runtime 或远程模型。 ### 部署默认值观测 {#deployment-default-observations} diff --git a/example/parsar/README.md b/example/parsar/README.md index 819d6e149..aabe38951 100644 --- a/example/parsar/README.md +++ b/example/parsar/README.md @@ -17,7 +17,7 @@ Open http://127.0.0.1:18180. The Core URL is an origin without `/v1`; remote ori ## Main flow -1. **Models:** enter a Provider name, Base URL and API key, then fetch its `GET /v1/models` list. Select models with checkboxes or add custom model IDs, then save the Provider and selected models together. The dialog shows discovered and selected counts. For manual entry, skip discovery. No default group is created. Keys are stored in the local restricted SQLite file and never returned to the browser; leaving the key blank while editing preserves it. Discovery sends only that Provider's key and does not follow redirects. The list expects the OpenAI-shaped `data: [{id: "..."}]` response. Discovery failures leave manual entry available. Codex and Claude Code workspace Sessions pass the selected Provider URL/key explicitly to Core, using the harness protocol. These Sessions require an HTTPS Base URL and API key. Text-only Sessions and hosted MiniMax Code use Core's deployment connection; the creation dialog states that their Provider is only a catalog group. The example does not yet expose MiniMax Code's required token limits. +1. **Models:** enter a Provider name, Base URL and API key, then fetch its `GET /v1/models` list. Select models with checkboxes or add custom model IDs, then save the Provider and selected models together. The dialog shows discovered and selected counts. For manual entry, skip discovery. No default group is created. Keys are stored in the local restricted SQLite file and never returned to the browser; leaving the key blank while editing preserves it. Discovery sends only that Provider's key and does not follow redirects. The list expects the OpenAI-shaped `data: [{id: "..."}]` response. Discovery failures leave manual entry available. Codex and Claude Code workspace Sessions pass the selected Provider URL/key explicitly to Core, using the harness protocol. These Sessions require an HTTPS Base URL and API key. Text-only Sessions and hosted MiniMax Code use Core's deployment connection; the creation dialog states that their Provider is only a catalog group. 2. **Skills:** create a SKILL.md resource or upload a ZIP. Inspect versions, upload a new version, and choose the default. Core validates and stores bundles. 3. **MCP:** save anonymous HTTPS endpoints and bind them to Agents. Hosted Sessions install an inline environment Plugin; text-only Sessions use Core's service-origin MCP. Hosted HTTP MCP supports Claude Code and Codex, not MiniMax Code. Authentication, Vault management and OAuth setup are not included. 4. **Runtimes:** choose a Core-managed sandbox, text-only environment, or user machine. User machines select Linux/macOS/Windows and an existing absolute workspace path, plus an optional local capability directory. Each Session gets its own daemon installation and credential; choosing the same workspace path shares the host's files, so use distinct paths for isolated work. These are placement configurations, not online machine identities. Machine enrollment, fixed-node routing and self-hosted registration are omitted. @@ -66,6 +66,6 @@ Create a user-machine runtime and start a Session with an Agent using Codex or C The selected model Provider needs an HTTPS Base URL and API key. Its protocol is Responses for Codex and Anthropic for Claude Code. Core freezes and delivers the configuration to the enrolled executor. Local pending creation requests contain that configuration but browser Session responses omit the entire pending request. -Self-hosted capability sources are local directories under the current Core API. Managed Skill bindings fail explicitly; configure local Skills/Plugins in the runtime's capability directory before connecting. Bound HTTP MCP services also fail explicitly for self-hosted Sessions; configure them in local Plugins instead. MiniMax Code self-hosted configuration is not included in this example because its required token limits are not exposed. +Self-hosted capability sources are local directories under the current Core API. Managed Skill bindings fail explicitly; configure local Skills/Plugins in the runtime's capability directory before connecting. Bound HTTP MCP services also fail explicitly for self-hosted Sessions; configure them in local Plugins instead. MiniMax Code self-hosted configuration is not included in this example. The daemon runs with the starting user's permissions and adds no sandbox. Runtime home is separate per Session; stopping it preserves local files. Credential rotation and revocation remain Core console operations. Reuse the original installation directory when reconnecting. diff --git a/internal/harnessconfig/configuration_test.go b/internal/harnessconfig/configuration_test.go index 8c0e072c7..95affb60d 100644 --- a/internal/harnessconfig/configuration_test.go +++ b/internal/harnessconfig/configuration_test.go @@ -12,16 +12,16 @@ func testDeclaration(kind string, configuration Configuration) Declaration { } func TestRegistryOwnsDeclarations(t *testing.T) { - declaration := testDeclaration("additional-adapter", Configuration{Providers: []Provider{{Protocol: "responses", RequiresTokenLimits: true}}}) + declaration := testDeclaration("additional-adapter", Configuration{Providers: []Provider{{Protocol: "responses"}}}) registry := NewRegistry(declaration) declaration.Providers[0].Protocol = "changed" got, ok := registry.Lookup("additional-adapter") - if !ok || got.Validate("responses", 100, 20) != nil || got.Validate("responses", 0, 0) == nil || got.ValidateProtocol("unknown") == nil { + if !ok || got.ValidateProtocol("responses") != nil || got.ValidateProtocol("unknown") == nil { t.Fatal("registry did not preserve adapter rules") } - got.Providers[0].RequiresTokenLimits = false + got.Providers[0].Protocol = "mutated" again, _ := registry.Lookup("additional-adapter") - if again.Validate("responses", 0, 0) == nil { + if again.ValidateProtocol("responses") != nil { t.Fatal("lookup mutated the registered declaration") } if _, ok := registry.Lookup("unknown"); ok || registry.SupportsProtocol("changed") || !registry.SupportsProtocol("responses") { diff --git a/internal/harnessconfig/harness.go b/internal/harnessconfig/harness.go index 7a01bbb84..7f259eace 100644 --- a/internal/harnessconfig/harness.go +++ b/internal/harnessconfig/harness.go @@ -45,8 +45,7 @@ import ( // Provider is deliberately limited to configuration compatibility. Core owns // credential admission and endpoint security; native launch options stay private. type Provider struct { - Protocol string - RequiresTokenLimits bool + Protocol string } // Configuration is an adapter-owned declaration, not live provider readiness. @@ -244,17 +243,6 @@ func (c Configuration) ValidateProtocol(protocol string) error { return nil } -func (c Configuration) Validate(protocol string, contextWindow, maxOutputTokens int32) error { - if err := c.ValidateProtocol(protocol); err != nil { - return err - } - provider, _ := c.Provider(protocol) - if provider.RequiresTokenLimits && (contextWindow <= 0 || maxOutputTokens <= 0) { - return errors.New("selected harness requires positive model context_window and max_output_tokens") - } - return nil -} - // Registry is an immutable snapshot. Missing declarations remain unknown rather // than inheriting another harness's rules. type Registry struct { @@ -297,11 +285,6 @@ func (r Registry) ValidateProtocol(kind, protocol string) error { return declaration.ValidateProtocol(protocol) } -func (r Registry) Validate(kind, protocol string, contextWindow, maxOutputTokens int32) error { - declaration, _ := r.Lookup(kind) - return declaration.Configuration.Validate(protocol, contextWindow, maxOutputTokens) -} - func (r Registry) SupportsProtocol(protocol string) bool { for _, declaration := range r.declarations { if _, ok := declaration.Provider(protocol); ok { diff --git a/internal/harnessconfig/mcode/declaration.go b/internal/harnessconfig/mcode/declaration.go index 5824704c5..befbf4b51 100644 --- a/internal/harnessconfig/mcode/declaration.go +++ b/internal/harnessconfig/mcode/declaration.go @@ -17,9 +17,9 @@ var Declaration = harnessconfig.Declaration{ Kind: "mcode", Label: "MiniMax Code", Configuration: harnessconfig.Configuration{Providers: []harnessconfig.Provider{ - {Protocol: "anthropic", RequiresTokenLimits: true}, - {Protocol: "responses", RequiresTokenLimits: true}, - {Protocol: "chat_completions", RequiresTokenLimits: true}, + {Protocol: "anthropic"}, + {Protocol: "responses"}, + {Protocol: "chat_completions"}, }}, Capabilities: proto.AgentKindCapabilities{ SubagentObservations: proto.CapabilitySupported, diff --git a/internal/harnessconfig/provider.go b/internal/harnessconfig/provider.go index 919809935..794501705 100644 --- a/internal/harnessconfig/provider.go +++ b/internal/harnessconfig/provider.go @@ -11,7 +11,7 @@ func (c Configuration) ParseProvider(raw any) (modelprovider.Provider, error) { if err != nil { return modelprovider.Provider{}, err } - if err := c.Validate(string(provider.Protocol), provider.ContextWindow, provider.MaxOutputTokens); err != nil { + if err := c.ValidateProtocol(string(provider.Protocol)); err != nil { return modelprovider.Provider{}, err } return provider, nil diff --git a/packages/agents-client/src/admin-client.test.ts b/packages/agents-client/src/admin-client.test.ts index 385518bf5..58b0e8395 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, token_limits_required: false } }, - { object: "core.harness", id: "codex", enabled: true, default: true, model_configuration: provider, model_configuration_support: { protocols: ["responses"], accepts_harness_config: true, token_limits_required: false } }, + { 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 } }, ] }; 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 321107239..9e6e3cae3 100644 --- a/packages/agents-client/src/admin-projection.ts +++ b/packages/agents-client/src/admin-projection.ts @@ -226,10 +226,10 @@ 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" || typeof support.token_limits_required !== "boolean") return invalidAdminResponse(); + typeof support.accepts_harness_config !== "boolean") return invalidAdminResponse(); return { protocols: [...protocols], - accepts_harness_config: support.accepts_harness_config, token_limits_required: support.token_limits_required, + accepts_harness_config: support.accepts_harness_config, }; } 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 5d8189d73..b254e58f7 100644 --- a/packages/agents-client/src/generated/core-api.ts +++ b/packages/agents-client/src/generated/core-api.ts @@ -406,9 +406,8 @@ export const modelConfigurationInputRequired = ["model", "model_provider"] as co export interface ModelConfigurationSupport { accepts_harness_config: boolean; protocols: string[]; - token_limits_required: boolean; } -export const modelConfigurationSupportFields = ["accepts_harness_config", "protocols", "token_limits_required"] as const; +export const modelConfigurationSupportFields = ["accepts_harness_config", "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 a910be149..e19e35b97 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, token_limits_required: false }; + const support = { protocols: ["responses"], accepts_harness_config: true }; 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, token_limits_required: true } }; + const minimax = { ...harness, id: "mcode", model_configuration_support: { protocols: ["anthropic", "responses", "chat_completions"], accepts_harness_config: false } }; const value = { object: "list", data: [harness, claude, minimax] }; expect(projectCoreHarnessList(value)).toEqual(value); const projected = projectCoreHarnessList(value).data[2]!.model_configuration_support.protocols; @@ -32,8 +32,8 @@ describe("adapter model configuration support", () => { { ...support, protocols: "responses" }, { ...support, protocols: [] }, { ...support, protocols: ["unknown"] }, { ...support, protocols: [null] }, { ...support, protocols: ["responses", "responses"] }, { ...support, native_protocols: ["responses"] }, { ...support, accepts_harness_config: "true" }, - { ...support, accepts_harness_config: undefined }, { ...support, token_limits_required: undefined }, - { ...support, token_limits_required: null }, { ...support, ready: true }]) { + { ...support, accepts_harness_config: undefined }, { ...support, token_limits_required: false }, + { ...support, ready: true }]) { expect(() => projectCoreHarnessList({ object: "list", data: [{ ...harness, model_configuration_support: value }] })).toThrow(); } }); diff --git a/scripts/acceptance/acceptance.py b/scripts/acceptance/acceptance.py index 078ddeeb2..4b01a1398 100644 --- a/scripts/acceptance/acceptance.py +++ b/scripts/acceptance/acceptance.py @@ -115,9 +115,6 @@ def settings(args): for name in ("context_window", "max_output_tokens"): require(type(provider.get(name, 0)) is int and provider.get(name, 0) >= 0, "invalid_model_limits") - if PROVIDERS[harness][protocol]["requires_token_limits"]: - require(provider.get("context_window", 0) > 0 and provider.get("max_output_tokens", 0) > 0, - "harness_requires_positive_model_limits") if provider.get("context_window") and provider.get("max_output_tokens"): require(provider["max_output_tokens"] <= provider["context_window"], "invalid_model_limits") secrets.append(key) diff --git a/scripts/acceptance/harness_catalog.py b/scripts/acceptance/harness_catalog.py index dd2b5c54c..79d8b6f57 100644 --- a/scripts/acceptance/harness_catalog.py +++ b/scripts/acceptance/harness_catalog.py @@ -1,7 +1,5 @@ # Code generated by scripts/generate-harness-catalog.py; DO NOT EDIT. -# Provider support comes from the Go Harness declarations. -PROVIDERS = {'claude_sdk': {'anthropic': {'requires_token_limits': False}}, - 'codex': {'responses': {'requires_token_limits': False}}, - 'mcode': {'anthropic': {'requires_token_limits': True}, - 'chat_completions': {'requires_token_limits': True}, - 'responses': {'requires_token_limits': True}}} +# Protocol support comes from the Go Harness declarations. +PROVIDERS = {'claude_sdk': ['anthropic'], + 'codex': ['responses'], + 'mcode': ['anthropic', 'responses', 'chat_completions']} diff --git a/scripts/acceptance/test_acceptance.py b/scripts/acceptance/test_acceptance.py index fcb8f5e59..862551b7d 100644 --- a/scripts/acceptance/test_acceptance.py +++ b/scripts/acceptance/test_acceptance.py @@ -37,7 +37,7 @@ def test_catalog_support_does_not_expand_campaign(self): with self.subTest(protocol=protocol), self.assertRaisesRegex( acceptance.Failure, "provider_protocol_does_not_match_campaign"): self.settings() - expanded = {**acceptance.PROVIDERS, "additional": {"responses": {"requires_token_limits": False}}} + expanded = {**acceptance.PROVIDERS, "additional": ["responses"]} with mock.patch.object(acceptance, "PROVIDERS", expanded): self.models["mcode"]["model_provider"]["protocol"] = "anthropic" self.settings() @@ -53,22 +53,12 @@ def test_unsupported_campaign_is_rejected_before_reading_secrets(self): acceptance.settings(SimpleNamespace()) private_file.assert_not_called() - def test_token_limit_requirements_follow_declarations(self): + def test_token_limits_are_optional(self): for harness in acceptance.CAMPAIGN: models = copy.deepcopy(self.models) del models[harness]["model_provider"]["context_window"] del models[harness]["model_provider"]["max_output_tokens"] - if harness == "mcode": - with self.assertRaisesRegex(acceptance.Failure, "harness_requires_positive_model_limits"): - self.settings(models) - else: - self.settings(models) - providers = copy.deepcopy(acceptance.PROVIDERS) - providers["codex"]["responses"]["requires_token_limits"] = True - del self.models["codex"]["model_provider"]["context_window"] - with mock.patch.object(acceptance, "PROVIDERS", providers): - with self.assertRaisesRegex(acceptance.Failure, "harness_requires_positive_model_limits"): - self.settings() + self.settings(models) def test_model_limit_validation(self): for field in ("context_window", "max_output_tokens"): @@ -78,11 +68,6 @@ def test_model_limit_validation(self): with self.subTest(field=field, value=value), self.assertRaisesRegex( acceptance.Failure, "invalid_model_limits"): self.settings(models) - for field in ("context_window", "max_output_tokens"): - models = copy.deepcopy(self.models) - models["mcode"]["model_provider"][field] = 0 - with self.assertRaisesRegex(acceptance.Failure, "harness_requires_positive_model_limits"): - self.settings(models) self.models["codex"]["model_provider"]["max_output_tokens"] = 64001 with self.assertRaisesRegex(acceptance.Failure, "invalid_model_limits"): self.settings() diff --git a/scripts/generate-harness-catalog.py b/scripts/generate-harness-catalog.py index 123c75951..b159e0c89 100644 --- a/scripts/generate-harness-catalog.py +++ b/scripts/generate-harness-catalog.py @@ -41,10 +41,10 @@ def go(source): check=True, capture_output=True).stdout -def render_installer(providers): +def render_installer(protocols): return ("# Code generated by scripts/generate-harness-catalog.py; DO NOT EDIT.\n" - "# Provider support comes from the Go Harness declarations.\n" - "PROVIDERS = " + pformat(providers, sort_dicts=True, width=100) + "\n") + "# Protocol support comes from the Go Harness declarations.\n" + "PROVIDERS = " + pformat(protocols, sort_dicts=True, width=100) + "\n") def render_client(harnesses, protocols): @@ -160,7 +160,7 @@ def projection(relative, content): raise SystemExit("Registered Harnesses differ from the declaration packages") for relative, content in render_docs(harnesses).items(): projection(relative, content) - projection(Path("scripts/acceptance/harness_catalog.py"), render_installer({h["kind"]: h["providers"] for h in harnesses})) + projection(Path("scripts/acceptance/harness_catalog.py"), render_installer({h["kind"]: h["protocols"] for h in harnesses})) projection(Path("packages/agents-client/src/harness-catalog.ts"), render_client(harnesses, declared["protocols"])) if stale: raise SystemExit("Stale Harness catalog projections; run make generate-harness-catalog:\n" + "\n".join(stale)) diff --git a/scripts/generate-harness-catalog.test.py b/scripts/generate-harness-catalog.test.py index 237a0a60f..b54776f97 100644 --- a/scripts/generate-harness-catalog.test.py +++ b/scripts/generate-harness-catalog.test.py @@ -22,11 +22,10 @@ def module(name, file): class HarnessCatalogTests(unittest.TestCase): def test_installer_projection_includes_every_declaration(self): - providers = {"additional": {"responses": {"requires_token_limits": True}, - "anthropic": {"requires_token_limits": False}}, "native": {}} + protocols = {"additional": ["responses", "anthropic"], "native": []} namespace = {} - exec(catalog.render_installer(providers), namespace) - self.assertEqual(namespace["PROVIDERS"], providers) + exec(catalog.render_installer(protocols), namespace) + self.assertEqual(namespace["PROVIDERS"], protocols) def test_new_declaration_reaches_all_projections(self): harnesses = [{"kind": "example", "label": "Example", "package": "example"}] diff --git a/scripts/harness-catalog/main.go b/scripts/harness-catalog/main.go index dc6b40e70..d12aafc0c 100644 --- a/scripts/harness-catalog/main.go +++ b/scripts/harness-catalog/main.go @@ -10,25 +10,21 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" ) -type provider struct { - RequiresTokenLimits bool `json:"requires_token_limits"` -} - type harness struct { - Kind string `json:"kind"` - Label string `json:"label"` - Providers map[string]provider `json:"providers"` + Kind string `json:"kind"` + Label string `json:"label"` + Protocols []string `json:"protocols"` } func declarations() []harness { var result []harness for _, kind := range builtin.Kinds() { declaration, _ := builtin.Registry().Lookup(kind) - providers := make(map[string]provider) + protocols := []string{} for _, declared := range declaration.Providers { - providers[declared.Protocol] = provider{RequiresTokenLimits: declared.RequiresTokenLimits} + protocols = append(protocols, declared.Protocol) } - result = append(result, harness{Kind: kind, Label: declaration.Label, Providers: providers}) + result = append(result, harness{Kind: kind, Label: declaration.Label, Protocols: protocols}) } return result } diff --git a/scripts/harness-catalog/main_test.go b/scripts/harness-catalog/main_test.go index 8225d6123..cbd6e1a7f 100644 --- a/scripts/harness-catalog/main_test.go +++ b/scripts/harness-catalog/main_test.go @@ -13,12 +13,12 @@ func TestProjectionMatchesEveryDeclaration(t *testing.T) { } for _, projected := range got { declaration, ok := builtin.Registry().Lookup(projected.Kind) - if !ok || projected.Label != declaration.Label || len(projected.Providers) != len(declaration.Providers) { + if !ok || projected.Label != declaration.Label || len(projected.Protocols) != len(declaration.Providers) { t.Fatalf("%s: projection differs from its declaration", projected.Kind) } - for _, declared := range declaration.Providers { - if projected.Providers[declared.Protocol].RequiresTokenLimits != declared.RequiresTokenLimits { - t.Fatalf("%s/%s: configuration rule differs", projected.Kind, declared.Protocol) + for i, declared := range declaration.Providers { + if projected.Protocols[i] != declared.Protocol { + t.Fatalf("%s: protocol order differs", projected.Kind) } } } diff --git a/services/core/deploy/mcode/README.md b/services/core/deploy/mcode/README.md index 6e7c0cd93..4360f8965 100644 --- a/services/core/deploy/mcode/README.md +++ b/services/core/deploy/mcode/README.md @@ -12,7 +12,7 @@ Workspace execution also requires the companion's readiness report: private prot ## Model provider -MiniMax Code accepts the `anthropic`, `responses` and `chat_completions` protocols, and each requires the provider's context window and maximum output tokens ([`harnessconfig/mcode`](../../../../internal/harnessconfig/mcode/declaration.go)). The adapter writes the frozen bundle as the native custom provider for the Session's exact `agent.model`, with those limits, using the native `anthropic-messages`, `openai-responses` or `openai-completions` API ([`model_provider.go`](../../../../apps/daemon/internal/agent/mcode/model_provider.go)). It selects that provider explicitly; there is no native account fallback. [Model execution](../../../../contracts/agents-api/model-execution.md#deployment-defaults) owns provider selection. +MiniMax Code accepts the `anthropic`, `responses` and `chat_completions` protocols, and the provider's context window and maximum output tokens are optional ([`harnessconfig/mcode`](../../../../internal/harnessconfig/mcode/declaration.go)). The adapter writes the frozen bundle as the native custom provider for the Session's exact `agent.model`, with the limits that are set, using the native `anthropic-messages`, `openai-responses` or `openai-completions` API ([`model_provider.go`](../../../../apps/daemon/internal/agent/mcode/model_provider.go)). It selects that provider explicitly; there is no native account fallback. A limit that is not set is omitted from the native configuration, so MiniMax Code applies its own defaults of 200000 context tokens and 16384 output tokens. [Model execution](../../../../contracts/agents-api/model-execution.md#deployment-defaults) owns provider selection. ## Execution profiles diff --git a/services/core/internal/api/core_model_provider_validation_test.go b/services/core/internal/api/core_model_provider_validation_test.go index f4d01ac45..932cddf7f 100644 --- a/services/core/internal/api/core_model_provider_validation_test.go +++ b/services/core/internal/api/core_model_provider_validation_test.go @@ -54,8 +54,6 @@ func TestCoreModelProviderValidationFields(t *testing.T) { {"protocol", "codex", `{"protocol":"private-protocol","base_url":"https://example.test","api_key":"private-key"}`, "model_provider_protocol_unsupported", "protocol", map[string]any{"harness": "codex", "allowed_protocols": []any{"responses"}}}, {"key", "codex", `{"protocol":"responses","base_url":"https://example.test","api_key":"private-key\n"}`, "model_provider_api_key_invalid", "api_key", map[string]any{"max_length": float64(16384)}}, {"output", "codex", `{"protocol":"responses","base_url":"https://example.test","api_key":"private-key","context_window":1,"max_output_tokens":2}`, "model_provider_token_limits_invalid", "max_output_tokens", nil}, - {"context required", "mcode", `{"protocol":"anthropic","base_url":"https://example.test","api_key":"private-key"}`, "model_provider_token_limits_invalid", "context_window", nil}, - {"output required", "mcode", `{"protocol":"anthropic","base_url":"https://example.test","api_key":"private-key","context_window":2}`, "model_provider_token_limits_invalid", "max_output_tokens", nil}, } { t.Run(tc.name, func(t *testing.T) { out := projectKeyHTTP(h, http.MethodPut, "/core/v1/harnesses/"+tc.harness+"/model-configuration", "admin", `{"model":"fixture","model_provider":`+tc.body+`}`) diff --git a/services/core/internal/api/harness_model_providers.go b/services/core/internal/api/harness_model_providers.go index bdf7f07c0..cf2e68af8 100644 --- a/services/core/internal/api/harness_model_providers.go +++ b/services/core/internal/api/harness_model_providers.go @@ -99,7 +99,6 @@ func (h *Handler) listHarnesses(w http.ResponseWriter, r *http.Request) { support := v1.ModelConfigurationSupport{Protocols: []string{}, AcceptsHarnessConfig: declaration.AcceptsHarnessConfig()} for _, provider := range declaration.Providers { support.Protocols = append(support.Protocols, provider.Protocol) - support.TokenLimitsRequired = support.TokenLimitsRequired || provider.RequiresTokenLimits } harness := CoreHarness{ModelConfigurationSupport: support, Object: "core.harness", ID: kind, Enabled: kind == h.Engine || h.harnesses[kind], Default: kind == h.Engine} for _, provider := range providers { diff --git a/website/.vitepress/theme/components/ComposeLab.vue b/website/.vitepress/theme/components/ComposeLab.vue index 225aa6cec..a266247a8 100644 --- a/website/.vitepress/theme/components/ComposeLab.vue +++ b/website/.vitepress/theme/components/ComposeLab.vue @@ -56,10 +56,6 @@ const lines = computed(() => { [{ t: ' ' }, s('x_agents_core'), { t: ': {' }, s('model_provider'), { t: ': {' }], [{ t: ' ' }, s('protocol'), { t: ': ' }, pair(protocol.value), { t: ',' }], [{ t: ' ' }, s('base_url'), { t: ': BASE_URL, ' }, s('api_key'), { t: ': API_KEY,' }], - // MiniMax Code requires positive token limits (contracts/agents-api/model-execution.md). - ...(harness.value === 'mcode' - ? [[{ t: ' ' }, s('context_window'), { t: ': ' }, { t: '200000' }, { t: ', ' }, s('max_output_tokens'), { t: ': ' }, { t: '8000' }, { t: ',' }]] - : []), [{ t: ' }},' }], [{ t: ' },' }], [{ t: ')' }], From f6e7661cfe0d94ddabf9eb3d90525ee3b51f7d05 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Mon, 19 Oct 2026 09:09:36 +0000 Subject: [PATCH 2/3] Update tests that required MiniMax Code token limits --- apps/web/e2e/system.spec.ts | 18 +++++++++--------- .../internal/execution/model_execution_test.go | 2 +- .../deployment_model_providers_http_test.go | 1 - 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/apps/web/e2e/system.spec.ts b/apps/web/e2e/system.spec.ts index 4e6d6eefa..ade0943e2 100644 --- a/apps/web/e2e/system.spec.ts +++ b/apps/web/e2e/system.spec.ts @@ -121,18 +121,18 @@ test("sets, replaces and clears a harness's default model configuration, and kee await claudeReplace.getByRole("button", { name: "Cancel" }).click(); expect(await page.content()).not.toContain(KEY); - // MiniMax Code needs both limits; the form shows Core's reason. A disabled harness may still be configured. + // A disabled harness may still be configured; token limits are optional. await mcode.getByRole("button", { name: "Set the default model configuration for MiniMax Code" }).click(); - const limits = page.getByRole("dialog", { name: "Set default model configuration for MiniMax Code" }); - await expect(limits.getByRole("combobox", { name: "Protocol", exact: true })).toHaveText("Anthropic Messages"); - await limits.getByRole("combobox", { name: "Protocol", exact: true }).click(); + const mcodeSet = page.getByRole("dialog", { name: "Set default model configuration for MiniMax Code" }); + await expect(mcodeSet.getByRole("combobox", { name: "Protocol", exact: true })).toHaveText("Anthropic Messages"); + await mcodeSet.getByRole("combobox", { name: "Protocol", exact: true }).click(); await expect(page.getByRole("option")).toHaveText(["Anthropic Messages", "OpenAI Responses", "OpenAI Chat Completions"]); await page.getByRole("option", { name: "OpenAI Responses", exact: true }).click(); - 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"); - await expect(limits.getByRole("button", { name: "Save" })).toBeDisabled(); - await limits.getByRole("button", { name: "Cancel" }).click(); + await mcodeSet.getByLabel("Base URL").fill("https://model.example/anthropic"); + await mcodeSet.getByLabel("API key").fill(KEY); + await mcodeSet.getByLabel("Default model ID").fill("fixture-model"); + await expect(mcodeSet.getByRole("button", { name: "Save" })).toBeEnabled(); + await mcodeSet.getByRole("button", { name: "Cancel" }).click(); await expect(mcode).toContainText("Not set"); expect(await writes(request)).toEqual([ diff --git a/services/core/internal/execution/model_execution_test.go b/services/core/internal/execution/model_execution_test.go index b7da9a67e..385c44683 100644 --- a/services/core/internal/execution/model_execution_test.go +++ b/services/core/internal/execution/model_execution_test.go @@ -62,7 +62,7 @@ func TestSessionModelOptionsPreserveUpstreamBundleForEveryHarness(t *testing.T) func TestSessionModelOptionsValidateAdmission(t *testing.T) { provider := &v1.ModelProviderInput{Protocol: "responses", BaseURL: "https://example.com/v1", APIKey: "private-key"} - for _, engine := range []string{"mcode", "unknown", ""} { + for _, engine := range []string{"unknown", ""} { if _, err := resolvedSessionModelOptions(provider, engine); err == nil { t.Fatalf("invalid provider/harness configuration accepted for %q", engine) } diff --git a/services/core/tests/integration/deployment_model_providers_http_test.go b/services/core/tests/integration/deployment_model_providers_http_test.go index 64344aa6d..3318c0ad6 100644 --- a/services/core/tests/integration/deployment_model_providers_http_test.go +++ b/services/core/tests/integration/deployment_model_providers_http_test.go @@ -111,7 +111,6 @@ func TestDeploymentModelProvidersHTTP(t *testing.T) { } { call("PUT", path, coreKey, `{"model":"fixture","model_provider":`+invalid+`}`, 400) } - call("PUT", "/core/v1/harnesses/mcode/model-configuration", coreKey, `{"model":"fixture","model_provider":{"protocol":"anthropic","base_url":"https://deployment.example/anthropic","api_key":"invalid-canary"}}`, 400) // Without any provider, hosted and self-hosted creation fail before any write. hosted := `{"agent":{"model":"hosted-model"},"environment":{"type":"openai_hosted"}}` From bb91b548005c8fc83cfca04e4916fc8a1014e054 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Mon, 19 Oct 2026 10:08:40 +0000 Subject: [PATCH 3/3] Drop the remaining required-limit rules and docs --- .../internal/agent/mcode/model_provider_test.go | 2 +- apps/web/e2e/fixture-console.mjs | 3 +-- apps/web/e2e/system.spec.ts | 16 ++++++++-------- contracts/agents-api/core-errors.md | 4 ++-- contracts/agents-api/zh/core-errors.md | 6 +++--- docs/api/public-agent-api.md | 2 +- docs/getting-started/install.md | 2 +- docs/zh/api/public-agent-api.md | 4 ++-- docs/zh/getting-started/install.md | 4 ++-- example/parsar/README.md | 6 +++--- example/parsar/server/sessions.mjs | 2 +- example/parsar/shared/session-profile.mjs | 2 -- example/parsar/src/NewSession.tsx | 14 ++++++-------- .../src/provider-observations.test.ts | 3 +-- scripts/acceptance/acceptance.py | 5 ----- scripts/acceptance/test_acceptance.py | 12 ------------ 16 files changed, 32 insertions(+), 55 deletions(-) diff --git a/apps/daemon/internal/agent/mcode/model_provider_test.go b/apps/daemon/internal/agent/mcode/model_provider_test.go index b466be76d..8da5b22c9 100644 --- a/apps/daemon/internal/agent/mcode/model_provider_test.go +++ b/apps/daemon/internal/agent/mcode/model_provider_test.go @@ -89,7 +89,7 @@ func TestOptionsRejectInvalidProvider(t *testing.T) { {"protocol alias", "protocol", "chat-completions"}, {"remote HTTP", "base_url", "http://provider.example/v1"}, {"empty key", "api_key", ""}, - {"missing context", "context_window", 0}, + {"output without context", "context_window", 0}, {"excess output", "max_output_tokens", 64001}, {"native options", "options", map[string]any{"apiKey": "fixture-key"}}, } { diff --git a/apps/web/e2e/fixture-console.mjs b/apps/web/e2e/fixture-console.mjs index 874a8b997..b81283af2 100644 --- a/apps/web/e2e/fixture-console.mjs +++ b/apps/web/e2e/fixture-console.mjs @@ -541,7 +541,6 @@ function providerProblem(harness, input) { 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 (harness === "mcode" && !(input.context_window > 0 && input.max_output_tokens > 0)) return "selected harness requires positive model context_window and max_output_tokens"; return null; } @@ -550,7 +549,7 @@ function providerProblem(harness, input) { * (services/core/internal/api/harness_model_providers.go): the list * reflects every write; an unknown harness or an unset provider is 404; PUT * takes a JSON object of at most 32 KiB, is a full replacement that needs the - * key every time (mcode also both limits) and answers with the safe view; + * key every time and answers with the safe view; * DELETE is 204 and safe to repeat. A disabled harness may still be configured. */ async function harnessRoute(request, response, path) { diff --git a/apps/web/e2e/system.spec.ts b/apps/web/e2e/system.spec.ts index ade0943e2..658d8be4e 100644 --- a/apps/web/e2e/system.spec.ts +++ b/apps/web/e2e/system.spec.ts @@ -123,16 +123,16 @@ test("sets, replaces and clears a harness's default model configuration, and kee // A disabled harness may still be configured; token limits are optional. await mcode.getByRole("button", { name: "Set the default model configuration for MiniMax Code" }).click(); - const mcodeSet = page.getByRole("dialog", { name: "Set default model configuration for MiniMax Code" }); - await expect(mcodeSet.getByRole("combobox", { name: "Protocol", exact: true })).toHaveText("Anthropic Messages"); - await mcodeSet.getByRole("combobox", { name: "Protocol", exact: true }).click(); + const limits = page.getByRole("dialog", { name: "Set default model configuration for MiniMax Code" }); + await expect(limits.getByRole("combobox", { name: "Protocol", exact: true })).toHaveText("Anthropic Messages"); + await limits.getByRole("combobox", { name: "Protocol", exact: true }).click(); await expect(page.getByRole("option")).toHaveText(["Anthropic Messages", "OpenAI Responses", "OpenAI Chat Completions"]); await page.getByRole("option", { name: "OpenAI Responses", exact: true }).click(); - await mcodeSet.getByLabel("Base URL").fill("https://model.example/anthropic"); - await mcodeSet.getByLabel("API key").fill(KEY); - await mcodeSet.getByLabel("Default model ID").fill("fixture-model"); - await expect(mcodeSet.getByRole("button", { name: "Save" })).toBeEnabled(); - await mcodeSet.getByRole("button", { name: "Cancel" }).click(); + 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"); + await expect(limits.getByRole("button", { name: "Save" })).toBeEnabled(); + await limits.getByRole("button", { name: "Cancel" }).click(); await expect(mcode).toContainText("Not set"); expect(await writes(request)).toEqual([ diff --git a/contracts/agents-api/core-errors.md b/contracts/agents-api/core-errors.md index febfd85e3..b97be3110 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 | Limits are invalid, or the Harness requires positive limits that are missing | +| `model_provider_token_limits_invalid` | `context_window` or `max_output_tokens` | omitted | A limit is negative, or `max_output_tokens` exceeds `context_window` (0 when omitted) | | `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 | @@ -69,7 +69,7 @@ Each code returns HTTP 400 with `type: "invalid_request_error"`. A missing, malf | `invalid_sandbox_configuration` | `resources.root_disk_mib` or `resources.environment_disk_mib` | `min`: 1024 for microsandbox; `min`: 0, `max`: 0 for Docker and E2B | Disk capacity is missing or unsupported by the provider | | `invalid_sandbox_configuration` | `runtime` | omitted | The Runtime release is missing, mutable, invalid or not allowed for E2B | -Bounds are validation constants, never submitted values. Node names are limited in bytes; Project and key names in trimmed Unicode characters without control characters. Only the first failure is reported, in this order: model provider URL, protocol, key, general limits, the Harness's protocol, then the Harness's required limits; sandbox resources CPU, memory, disk, then Runtime. Model-provider field errors inside a `model_provider` object keep that object's field as `param`. An unknown sandbox provider returns an error without these fields. +Bounds are validation constants, never submitted values. Node names are limited in bytes; Project and key names in trimmed Unicode characters without control characters. Only the first failure is reported, in this order: model provider URL, protocol, key, limits, then the Harness's protocol; sandbox resources CPU, memory, disk, then Runtime. Model-provider field errors inside a `model_provider` object keep that object's field as `param`. An unknown sandbox provider returns an error without these fields. ## Other administration errors diff --git a/contracts/agents-api/zh/core-errors.md b/contracts/agents-api/zh/core-errors.md index b0a1b3193..d5461517c 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: 8fdff312fc4a2140079fca076fd099c4329596f1f12859a6caa1a3b3ad3908fd +source_hash: 73b7865736df763df48d1b419594feec04ef1c05b57e3d8e0c176e54ab336b7c --- `/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` | 省略 | 限制无效,或 Harness 要求的正数限制缺失 | +| `model_provider_token_limits_invalid` | `context_window` 或 `max_output_tokens` | 省略 | 限制为负数,或 `max_output_tokens` 超过 `context_window`(省略时为 0) | | `model_configuration_model_invalid` | `model` | 省略 | 部署默认配置的 model 不是非空模型标识符 | | `harness_config_invalid` | `harness_config` | 省略 | 部署默认配置的原生参数不受支持或无效 | | `invalid_sandbox_configuration` | `resources.cpus` | `min`:1,`max`:255 | CPU 数量超出支持范围 | @@ -71,7 +71,7 @@ Web 的控制台服务器在 `/core` 路径上发生自身故障时使用此封 | `invalid_sandbox_configuration` | `resources.root_disk_mib` 或 `resources.environment_disk_mib` | `min`:microsandbox 为 1024;Docker 和 E2B 的 `min`:0,`max`:0 | 磁盘容量缺失或提供商不支持 | | `invalid_sandbox_configuration` | `runtime` | 省略 | Runtime 版本缺失、可变、无效或 E2B 不允许 | -这些边界是验证常量,绝不是提交的值。节点名称按字节数限制;项目名称和键名称按去除首尾空白后的 Unicode 字符数限制,且不得包含控制字符。系统仅按以下顺序报告第一个失败项:模型提供商 URL、协议、密钥、常规限制、Harness 协议,然后是 Harness 的必需限制;沙箱资源依次为 CPU、内存、磁盘,然后是 Runtime。`model_provider` 对象内的模型提供商字段错误仍以该对象的相应字段作为 `param`。未知的沙箱提供商返回一个不含这些字段的错误。 +这些边界是验证常量,绝不是提交的值。节点名称按字节数限制;项目名称和键名称按去除首尾空白后的 Unicode 字符数限制,且不得包含控制字符。系统仅按以下顺序报告第一个失败项:模型提供商 URL、协议、密钥、限制,然后是 Harness 协议;沙箱资源依次为 CPU、内存、磁盘,然后是 Runtime。`model_provider` 对象内的模型提供商字段错误仍以该对象的相应字段作为 `param`。未知的沙箱提供商返回一个不含这些字段的错误。 ## 其他管理错误 {#other-administration-errors} diff --git a/docs/api/public-agent-api.md b/docs/api/public-agent-api.md index ca4648b1e..607dbb04d 100644 --- a/docs/api/public-agent-api.md +++ b/docs/api/public-agent-api.md @@ -145,7 +145,7 @@ Core runs several Harnesses and accepts your own model access. Those settings ar | Field | Where | Value | | --- | --- | --- | | `harness` | Agent, or a Session's inline `agent` | `codex`, `claude_sdk` or `mcode` | -| `model_provider` | Agent, or Session creation (top level) | `protocol`, `base_url`, `api_key`, and for `mcode` also `context_window` and `max_output_tokens`. The protocol must be one of the Harness's native protocols | +| `model_provider` | Agent, or Session creation (top level) | `protocol`, `base_url`, `api_key`, and optionally `context_window` and `max_output_tokens`. The protocol must be one of the Harness's native protocols | | `harness_config` | Agent, inline `agent`, or Session creation (top level, wins) | The Harness's native model parameters, such as Claude Code's `thinking` | | `environment` | `openai_hosted` or `self_hosted` Session creation (top level) | Portable preparation: `environment_template_id`, `files`, `env`, `packages`, `setup_commands`, `skills`, `plugins`, `capability_directories`. A field may not also appear in `environment`; see [Environments](../../contracts/agents-api/environments.md#preparation-order) | | `installation` | Read-only, on `self_hosted` Sessions | Short-lived install commands for your machine; see [Run self-hosted Sessions](../getting-started/self-hosted.md) | diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index f90d059be..e035062d3 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -83,7 +83,7 @@ The installer selects no sandbox backend. [Choose a sandbox backend](./sandbox-b Core-hosted Sessions that bring no model provider use their Harness's default model. See [default models](../configuration.md#default-models). 1. Open **Settings → Models and credentials**. Under **Default model configuration**, the Harness marked **Default** shows **Model provider not set**. It is Codex unless you changed `OAC_DEFAULT_HARNESS`; see [settings](../configuration.md#settings). -2. Choose **Set** and enter the model ID, the protocol, and the provider's base URL and API key. MiniMax Code also needs the context window and the maximum output tokens. +2. Choose **Set** and enter the model ID, the protocol, and the provider's base URL and API key. 3. Save. The Harness shows **Ready**. ## Issue a Project API key diff --git a/docs/zh/api/public-agent-api.md b/docs/zh/api/public-agent-api.md index ceb337941..53dca8f76 100644 --- a/docs/zh/api/public-agent-api.md +++ b/docs/zh/api/public-agent-api.md @@ -1,7 +1,7 @@ --- title: "Agents API 指南" source: docs/api/public-agent-api.md -source_hash: 47c626b6be9a45d18e566d17556321a484356e825e0d5f0dd07385ccd6e50496 +source_hash: 83f4c5d7c0e5c748087e6baa51c792a7cd831e217b47980c65db7d04e9222140 --- Core 在 `/v1` 提供 OpenAI Agents API。本指南用官方 OpenAI SDK 和普通 HTTP 两种方式演示每项常见任务,并说明 Core 与 OpenAI 的差异。如果尚未启动过 Session,请先阅读[运行第一个 Session](../getting-started/quickstart.md)。 @@ -147,7 +147,7 @@ Core 可以运行多种 Harness,并允许接入你自己的模型访问方式 | 字段 | 所在位置 | 值 | | --- | --- | --- | | `harness` | Agent,或 Session 的内联 `agent` | `codex`、`claude_sdk` 或 `mcode` | -| `model_provider` | Agent,或 Session 创建请求(顶层) | `protocol`、`base_url`、`api_key`;对于 `mcode`,还包括 `context_window` 和 `max_output_tokens`。协议必须是该 Harness 的原生协议之一 | +| `model_provider` | Agent,或 Session 创建请求(顶层) | `protocol`、`base_url`、`api_key`,以及可选的 `context_window` 和 `max_output_tokens`。协议必须是该 Harness 的原生协议之一 | | `harness_config` | Agent、内联 `agent`,或 Session 创建请求(顶层,优先) | Harness 的原生模型参数,例如 Claude Code 的 `thinking` | | `environment` | `openai_hosted` 或 `self_hosted` Session 创建请求(顶层) | 可移植的准备配置:`environment_template_id`、`files`、`env`、`packages`、`setup_commands`、`skills`、`plugins`、`capability_directories`。同一字段不能也出现在 `environment` 中;请参阅 [Environments](../../../contracts/agents-api/zh/environments.md#preparation-order) | | `installation` | 只读,适用于 `self_hosted` Session | 在你的计算机上运行的短期安装命令;请参阅 [运行自托管 Session](../getting-started/self-hosted.md) | diff --git a/docs/zh/getting-started/install.md b/docs/zh/getting-started/install.md index 9f1d02689..1ee216f5d 100644 --- a/docs/zh/getting-started/install.md +++ b/docs/zh/getting-started/install.md @@ -1,7 +1,7 @@ --- title: "安装 Core 和 Web" source: docs/getting-started/install.md -source_hash: 6bde0267753b797a6ada321509d9ac4ab2270021069308f4f07bb33c9d3e03cd +source_hash: e638457e86e53dac6f049c96e1a1dbd91646a26b1b6179f90c5b39f8750425b5 --- 本指南带运维人员从一台空主机到可用的安装:在 Docker 下运行的 Core、Web 控制台和 PostgreSQL,一个沙箱提供商,一个默认模型和第一个项目 API 密钥。Web 的 **概览** 通过 **新手引导** 清单跟踪同样的步骤。 @@ -85,7 +85,7 @@ curl -fsSL https://openagentcore.dev/install.sh | bash -s -- --public-url https: 没有自带模型提供商的 Core 托管 Session 使用其 Harness 的默认模型。参阅[默认模型](../configuration.md#default-models)。 1. 打开 **设置 → 模型与凭据**。在 **默认模型配置** 下,标记为 **默认** 的 Harness 显示 **未设置模型提供商**。除非修改了 `OAC_DEFAULT_HARNESS`,否则为 Codex,参阅[设置](../configuration.md#settings)。 -2. 选择 **设置**,输入模型 ID、协议以及提供商的基础 URL 和 API 密钥。MiniMax Code 还需要上下文窗口和最大输出 token 数。 +2. 选择 **设置**,输入模型 ID、协议以及提供商的基础 URL 和 API 密钥。 3. 保存。该 Harness 显示 **就绪**。 ## 签发项目 API 密钥 {#issue-a-project-api-key} diff --git a/example/parsar/README.md b/example/parsar/README.md index aabe38951..2dbf5cbe2 100644 --- a/example/parsar/README.md +++ b/example/parsar/README.md @@ -17,7 +17,7 @@ Open http://127.0.0.1:18180. The Core URL is an origin without `/v1`; remote ori ## Main flow -1. **Models:** enter a Provider name, Base URL and API key, then fetch its `GET /v1/models` list. Select models with checkboxes or add custom model IDs, then save the Provider and selected models together. The dialog shows discovered and selected counts. For manual entry, skip discovery. No default group is created. Keys are stored in the local restricted SQLite file and never returned to the browser; leaving the key blank while editing preserves it. Discovery sends only that Provider's key and does not follow redirects. The list expects the OpenAI-shaped `data: [{id: "..."}]` response. Discovery failures leave manual entry available. Codex and Claude Code workspace Sessions pass the selected Provider URL/key explicitly to Core, using the harness protocol. These Sessions require an HTTPS Base URL and API key. Text-only Sessions and hosted MiniMax Code use Core's deployment connection; the creation dialog states that their Provider is only a catalog group. +1. **Models:** enter a Provider name, Base URL and API key, then fetch its `GET /v1/models` list. Select models with checkboxes or add custom model IDs, then save the Provider and selected models together. The dialog shows discovered and selected counts. For manual entry, skip discovery. No default group is created. Keys are stored in the local restricted SQLite file and never returned to the browser; leaving the key blank while editing preserves it. Discovery sends only that Provider's key and does not follow redirects. The list expects the OpenAI-shaped `data: [{id: "..."}]` response. Discovery failures leave manual entry available. Workspace Sessions pass the selected Provider URL/key explicitly to Core, using the harness protocol. These Sessions require an HTTPS Base URL and API key. Text-only Sessions use Core's deployment connection; the creation dialog states that their Provider is only a catalog group. 2. **Skills:** create a SKILL.md resource or upload a ZIP. Inspect versions, upload a new version, and choose the default. Core validates and stores bundles. 3. **MCP:** save anonymous HTTPS endpoints and bind them to Agents. Hosted Sessions install an inline environment Plugin; text-only Sessions use Core's service-origin MCP. Hosted HTTP MCP supports Claude Code and Codex, not MiniMax Code. Authentication, Vault management and OAuth setup are not included. 4. **Runtimes:** choose a Core-managed sandbox, text-only environment, or user machine. User machines select Linux/macOS/Windows and an existing absolute workspace path, plus an optional local capability directory. Each Session gets its own daemon installation and credential; choosing the same workspace path shares the host's files, so use distinct paths for isolated work. These are placement configurations, not online machine identities. Machine enrollment, fixed-node routing and self-hosted registration are omitted. @@ -64,8 +64,8 @@ It leaves sample resources for inspection and executes the configured model. It Create a user-machine runtime and start a Session with an Agent using Codex or Claude Code. No initial Turn is sent. Open **Connect user machine** and run the Core-provided command on the target host. The installer downloads the matching native distribution, installs the selected harness and starts the connection. Windows Claude Code also requires Git Bash. The page reads Core's public Environment status and enables sending after it reports connected. The example backend does not need or accept the administrator Core key. Commands carry temporary Environment-scoped authorization; do not share them. Session refresh renews them. -The selected model Provider needs an HTTPS Base URL and API key. Its protocol is Responses for Codex and Anthropic for Claude Code. Core freezes and delivers the configuration to the enrolled executor. Local pending creation requests contain that configuration but browser Session responses omit the entire pending request. +The selected model Provider needs an HTTPS Base URL and API key. Its protocol is Responses for Codex and Anthropic for Claude Code and MiniMax Code. Core freezes and delivers the configuration to the enrolled executor. Local pending creation requests contain that configuration but browser Session responses omit the entire pending request. -Self-hosted capability sources are local directories under the current Core API. Managed Skill bindings fail explicitly; configure local Skills/Plugins in the runtime's capability directory before connecting. Bound HTTP MCP services also fail explicitly for self-hosted Sessions; configure them in local Plugins instead. MiniMax Code self-hosted configuration is not included in this example. +Self-hosted capability sources are local directories under the current Core API. Managed Skill bindings fail explicitly; configure local Skills/Plugins in the runtime's capability directory before connecting. Bound HTTP MCP services also fail explicitly for self-hosted Sessions; configure them in local Plugins instead. The daemon runs with the starting user's permissions and adds no sandbox. Runtime home is separate per Session; stopping it preserves local files. Credential rotation and revocation remain Core console operations. Reuse the original installation directory when reconnecting. diff --git a/example/parsar/server/sessions.mjs b/example/parsar/server/sessions.mjs index 2cfdd72e6..46e4d5053 100644 --- a/example/parsar/server/sessions.mjs +++ b/example/parsar/server/sessions.mjs @@ -18,7 +18,7 @@ function requestFor(store, id, body) { const restriction = sessionRestriction(agent, runtime); if (restriction) throw new AppError(400, restriction); let modelProvider; - if (environment.type !== "none" && agent.harness !== "mcode") { + if (environment.type !== "none") { const provider = store.get("providers", model.provider_id); if (!provider?.base_url?.startsWith("https://") || !provider.api_key) throw new AppError( diff --git a/example/parsar/shared/session-profile.mjs b/example/parsar/shared/session-profile.mjs index 96ea469fd..e4ec5060d 100644 --- a/example/parsar/shared/session-profile.mjs +++ b/example/parsar/shared/session-profile.mjs @@ -6,8 +6,6 @@ export function sessionRestriction(agent, runtime) { return "用户机器使用本地能力目录,不能绑定托管 Skill。请先编辑 Agent,移除托管 Skill。"; if (agent.mcp_ids.length) return "用户机器的 MCP 需通过本地 Plugin 配置。请先编辑 Agent,移除 MCP 绑定。"; - if (agent.harness === "mcode") - return "当前示例的用户机器支持 Codex 和 Claude Code,请先编辑 Agent 的执行引擎。"; } if (runtime.environment === "none" && agent.skill_ids.length) return "Skills 需要托管运行环境,请选择托管沙箱或移除 Skill 绑定。"; diff --git a/example/parsar/src/NewSession.tsx b/example/parsar/src/NewSession.tsx index eefef33ff..b513d2f84 100644 --- a/example/parsar/src/NewSession.tsx +++ b/example/parsar/src/NewSession.tsx @@ -140,14 +140,12 @@ export function NewSession({ {restriction}

)} - {!restriction && - runtime && - (runtime.environment === "none" || agent.harness === "mcode") && ( -

- 此组合使用 Core 部署的模型连接;这里选择的 Provider - 仅用于模型分组。 -

- )} + {!restriction && runtime && runtime.environment === "none" && ( +

+ 此组合使用 Core 部署的模型连接;这里选择的 Provider + 仅用于模型分组。 +

+ )} {uncertain && (

如请求结果未确定,可关闭弹窗,在会话列表中恢复同一次创建。

diff --git a/packages/agents-client/src/provider-observations.test.ts b/packages/agents-client/src/provider-observations.test.ts index e19e35b97..bd5ef988a 100644 --- a/packages/agents-client/src/provider-observations.test.ts +++ b/packages/agents-client/src/provider-observations.test.ts @@ -32,8 +32,7 @@ describe("adapter model configuration support", () => { { ...support, protocols: "responses" }, { ...support, protocols: [] }, { ...support, protocols: ["unknown"] }, { ...support, protocols: [null] }, { ...support, protocols: ["responses", "responses"] }, { ...support, native_protocols: ["responses"] }, { ...support, accepts_harness_config: "true" }, - { ...support, accepts_harness_config: undefined }, { ...support, token_limits_required: false }, - { ...support, ready: true }]) { + { ...support, accepts_harness_config: undefined }, { ...support, ready: true }]) { expect(() => projectCoreHarnessList({ object: "list", data: [{ ...harness, model_configuration_support: value }] })).toThrow(); } }); diff --git a/scripts/acceptance/acceptance.py b/scripts/acceptance/acceptance.py index 4b01a1398..82856720c 100644 --- a/scripts/acceptance/acceptance.py +++ b/scripts/acceptance/acceptance.py @@ -112,11 +112,6 @@ def settings(args): key = provider["api_key"] require(isinstance(key, str) and key and len(key) <= 16384 and not any(c in key for c in "\r\n\0"), "invalid_model_provider_key") - for name in ("context_window", "max_output_tokens"): - require(type(provider.get(name, 0)) is int and provider.get(name, 0) >= 0, - "invalid_model_limits") - if provider.get("context_window") and provider.get("max_output_tokens"): - require(provider["max_output_tokens"] <= provider["context_window"], "invalid_model_limits") secrets.append(key) return token, models, secrets diff --git a/scripts/acceptance/test_acceptance.py b/scripts/acceptance/test_acceptance.py index 862551b7d..4b9fd3823 100644 --- a/scripts/acceptance/test_acceptance.py +++ b/scripts/acceptance/test_acceptance.py @@ -60,18 +60,6 @@ def test_token_limits_are_optional(self): del models[harness]["model_provider"]["max_output_tokens"] self.settings(models) - def test_model_limit_validation(self): - for field in ("context_window", "max_output_tokens"): - for value in (False, -1, "100", 1.5, None): - models = copy.deepcopy(self.models) - models["codex"]["model_provider"][field] = value - with self.subTest(field=field, value=value), self.assertRaisesRegex( - acceptance.Failure, "invalid_model_limits"): - self.settings(models) - self.models["codex"]["model_provider"]["max_output_tokens"] = 64001 - with self.assertRaisesRegex(acceptance.Failure, "invalid_model_limits"): - self.settings() - def test_provider_validation_is_preserved(self): for patch, label in ( ({"unknown": "value"}, "invalid_model_provider_fields"),