Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions apps/daemon/internal/agent/codex/provider_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ type providerConfig struct {
// Name is the human-readable label shown in codex UI; defaults to
// "OpenAgentCore" when empty.
Name string
// BaseURL is the model provider's HTTPS endpoint, including any
// path prefix (e.g. /v1). Required.
// BaseURL is the validated provider's base URL, including any path
// prefix (e.g. /v1).
BaseURL string
// BearerToken is the literal API key. Codex's `experimental_bearer_token`
// field accepts a string; we emit it verbatim. Required.
// field accepts a string; we emit it verbatim.
BearerToken string
// HTTPHeaders is forwarded as `[model_providers.oac.http_headers]`.
// Keys / values rendered as TOML basic strings.
Expand Down Expand Up @@ -57,13 +57,6 @@ type providerConfig struct {
// The provider block is rewritten on every prompt; manual edits to
// scratch CODEX_HOME files are lost on the next spawn.
func writeCodexProviderConfig(codexHome string, cfg providerConfig) error {
if strings.TrimSpace(cfg.BaseURL) == "" {
return fmt.Errorf("codex: provider base_url is required")
}
if strings.TrimSpace(cfg.BearerToken) == "" {
return fmt.Errorf("codex: provider bearer_token is required")
}

var b strings.Builder
b.WriteString("# Generated by oac-daemon (codex agent) — do not edit by hand.\n")
b.WriteString("# Rewritten on every prompt; manual changes will be lost.\n\n")
Expand Down
18 changes: 0 additions & 18 deletions apps/daemon/internal/agent/codex/provider_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,24 +92,6 @@ func TestWriteCodexProviderConfig_FullProvider(t *testing.T) {
}
}

func TestWriteCodexProviderConfig_RejectsMissingFields(t *testing.T) {
dir := t.TempDir()
cases := []struct {
name string
cfg providerConfig
}{
{"missing base_url", providerConfig{BearerToken: "sk-x"}},
{"missing bearer_token", providerConfig{BaseURL: "https://x"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if err := writeCodexProviderConfig(dir, tc.cfg); err == nil {
t.Fatal("expected error for incomplete provider config")
}
})
}
}

// TestWriteCodexProviderConfig_AppendsAlongsideMCP verifies the two
// writers coexist on the same file. Without append semantics one would
// silently overwrite the other depending on call order.
Expand Down
2 changes: 1 addition & 1 deletion apps/daemon/internal/agenthost/admit.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func admit(cfg Config, roots *x509.CertPool, req proto.PromptRequestPayload, env
return nil, unsupported("a Session without a frozen model provider")
}
provider := *req.ModelProvider
if err := provider.Validate(); err != nil {
if err := provider.Validate(false); err != nil {
return nil, invalidSession("model provider: %v", err)
}
bindings, err := agent.ResolveMCPBindings(req)
Expand Down
2 changes: 1 addition & 1 deletion apps/daemon/internal/gateway/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type modelRelay struct {
}

func newModelRelay(p modelprovider.Provider, t http.RoundTripper) (*modelRelay, error) {
if err := p.Validate(); err != nil {
if err := p.Validate(false); err != nil {
return nil, err
}
credential, err := modelprovider.UpstreamCredential(p.Protocol)
Expand Down
4 changes: 2 additions & 2 deletions contracts/agents-api/core-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ Each code returns HTTP 400 with `type: "invalid_request_error"`. A missing, malf
| `invalid_name` | `name` | `max_length`: 128 for Projects and nodes, 80 for Project keys | The name failed the resource's validator |
| `invalid_node_capacity` | `max_active` or `max_retained` | `min`: 1, `max`: 1000000 | Capacity is invalid; retained capacity must also be at least active capacity |
| `invalid_model_provider` | null | omitted | A complete model-provider bundle is required |
| `model_provider_base_url_invalid` | `base_url` | omitted | Requires HTTPS without credentials, query or fragment |
| `model_provider_base_url_invalid` | `base_url` | omitted | The base URL breaks the [provider rule](./model-execution.md#session-override) |
| `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_api_key_invalid` | `api_key` | `max_length`: 16384 | The key breaks the [provider rule](./model-execution.md#session-override) |
| `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_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 |
Expand Down
4 changes: 2 additions & 2 deletions contracts/agents-api/model-execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "Model execution"
---

Each Session runs one Harness with one model provider. Core selects them through three Core extensions that the pinned upstream protocol does not define: `x_agents_core.harness` chooses the Harness, `x_agents_core.model_provider` supplies the endpoint and key, and `x_agents_core.harness_config` carries native model parameters. Core has no provider catalog, model alias resolution or product permission model; besides Session and saved-Agent bundles, the only stored bundle is one [deployment default](#deployment-defaults) per Harness. This document is the Harness–model provider protocol: [`internal/modelprovider/config.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/modelprovider/config.go) validates the frozen provider connection and declares what the [credential gateway](#credential-gateway) relays, and each Harness declares its protocols and native parameters through [`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go).
Each Session runs one Harness with one model provider. Core selects them through three Core extensions that the pinned upstream protocol does not define: `x_agents_core.harness` chooses the Harness, `x_agents_core.model_provider` supplies the endpoint and key, and `x_agents_core.harness_config` carries native model parameters. Core has no provider catalog, model alias resolution or product permission model; besides Session and saved-Agent bundles, the only stored bundle is one [deployment default](#deployment-defaults) per Harness. This document is the Harness–model provider protocol: [`internal/modelprovider/config.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/modelprovider/config.go) defines the [provider rule](#session-override) that Core and the Runtime both apply and declares what the [credential gateway](#credential-gateway) relays, and each Harness declares its protocols and native parameters through [`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go).

## Harness selection

Expand Down Expand Up @@ -72,7 +72,7 @@ Every creation request records caller intent before resolving saved Agents, temp
```

- `protocol` names the upstream API (`anthropic`, `responses` or `chat_completions`), not an engine. The selected Harness must support it natively.
- `base_url` uses HTTPS with a valid host, without credentials, query or fragment. A loopback host means the agent host's network namespace, for every Environment type. For `anthropic` it excludes the version path, because the Harness appends `/v1/messages`, so a value ending in `/v1` or `/v1/` is rejected.
- `base_url` uses HTTPS with a valid host, without credentials, query or fragment. A loopback host means the agent host's network namespace, for every Environment type. Core rejects plain `http`; the Runtime accepts it only to a loopback IP address, for the credential gateway's listener that it hands the Harness. For `anthropic` it excludes the version path, because the Harness appends `/v1/messages`, so a value ending in `/v1` or `/v1/` is rejected.
- `api_key` is nonempty, at most 16 KiB and contains no NUL, CR or LF.
- `context_window` and `max_output_tokens` are optional nonnegative integers, with output no larger than context; both must be positive for MiniMax Code. Use the real model's limits.
- `agent.model` is the exact provider model ID; a supplied value always replaces the deployment model.
Expand Down
68 changes: 34 additions & 34 deletions contracts/agents-api/v1/model_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ package v1

import (
"encoding/json"
"net/url"
"strings"
"errors"

"github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig"
"github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/builtin"
"github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider"
)
Expand All @@ -19,6 +17,22 @@ type ModelProviderError struct {

func (e *ModelProviderError) Error() string { return e.message }

// modelProviderErrorCodes maps each modelprovider.FieldError field to its code.
var modelProviderErrorCodes = map[string]string{
"base_url": "model_provider_base_url_invalid",
"protocol": "model_provider_protocol_unsupported",
"api_key": "model_provider_api_key_invalid",
"context_window": "model_provider_token_limits_invalid",
"max_output_tokens": "model_provider_token_limits_invalid",
}

// Model provider sources, as recorded in a Session's execution configuration.
const (
ModelProviderSourceSession = "session"
ModelProviderSourceAgent = "agent"
ModelProviderSourceDeployment = "deployment"
)

// SessionExecutionInput is a write-only execution extension, not a provider resource.
type SessionExecutionInput struct {
// Environment supplies placement-independent preparation through the Core extension.
Expand All @@ -35,47 +49,33 @@ type ModelProviderInput struct {
MaxOutputTokens int32 `json:"max_output_tokens,omitempty"`
}

func (p *ModelProviderInput) Validate() error {
return p.validate(builtin.Registry())
// Provider is the bundle as the Harness–Model provider protocol carries it.
func (p *ModelProviderInput) Provider() modelprovider.Provider {
return modelprovider.Provider{Protocol: modelprovider.Protocol(p.Protocol), BaseURL: p.BaseURL, APIKey: p.APIKey,
ContextWindow: p.ContextWindow, MaxOutputTokens: p.MaxOutputTokens}
}

func (p *ModelProviderInput) validate(registry harnessconfig.Registry) error {
// Validate applies modelprovider's rule, without loopback http, and reports
// the rejected field with its public code.
func (p *ModelProviderInput) Validate() error {
if p == nil {
return &ModelProviderError{Code: "invalid_model_provider", Param: "", message: "model_provider is required"}
}
if !validModelProviderBaseURL(p.BaseURL) {
return &ModelProviderError{Code: "model_provider_base_url_invalid", Param: "base_url", message: "model provider requires an HTTPS base_url without credentials, query or fragment"}
}
if base, _ := url.Parse(p.BaseURL); !modelprovider.Protocol(p.Protocol).ValidBasePath(base.Path) {
return &ModelProviderError{Code: "model_provider_base_url_invalid", Param: "base_url", message: "an anthropic base_url excludes the /v1 version path"}
}
if !registry.SupportsProtocol(p.Protocol) {
return &ModelProviderError{Code: "model_provider_protocol_unsupported", Param: "protocol", message: "unsupported model provider protocol"}
}
if strings.TrimSpace(p.APIKey) == "" || len(p.APIKey) > 16384 || strings.ContainsAny(p.APIKey, "\x00\r\n") {
return &ModelProviderError{Code: "model_provider_api_key_invalid", Param: "api_key", message: "invalid model provider API key"}
return &ModelProviderError{Code: "invalid_model_provider", message: "model_provider is required"}
}
if p.ContextWindow < 0 || p.MaxOutputTokens < 0 || (p.MaxOutputTokens > p.ContextWindow) {
param := "max_output_tokens"
if p.ContextWindow < 0 {
param = "context_window"
}
return &ModelProviderError{Code: "model_provider_token_limits_invalid", Param: param, message: "invalid model token limits"}
err := p.Provider().Validate(false)
var field *modelprovider.FieldError
if !errors.As(err, &field) {
return err
}
return nil
return &ModelProviderError{Code: modelProviderErrorCodes[field.Field], Param: field.Field, message: field.Error()}
}

func (p *ModelProviderInput) ValidateHarness(harness string) error {
return p.ValidateHarnessWithRegistry(harness, builtin.Registry())
}

// ValidateHarnessWithRegistry validates provider input against adapter-owned rules.
// ValidateHarness also validates provider input against adapter-owned rules.
// It does not enable an execution engine or placement.
func (p *ModelProviderInput) ValidateHarnessWithRegistry(harness string, registry harnessconfig.Registry) error {
if err := p.validate(registry); err != nil {
func (p *ModelProviderInput) ValidateHarness(harness string) error {
if err := p.Validate(); err != nil {
return err
}
configuration, _ := registry.Lookup(harness)
configuration, _ := builtin.Registry().Lookup(harness)
if err := configuration.ValidateProtocol(p.Protocol); err != nil {
return &ModelProviderError{Code: "model_provider_protocol_unsupported", Param: "protocol", message: err.Error()}
}
Expand Down
11 changes: 0 additions & 11 deletions contracts/agents-api/v1/model_execution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,6 @@ func TestModelExecutionValidation(t *testing.T) {
t.Fatalf("unsupported protocol or harness accepted: %s/%s", tc.protocol, tc.harness)
}
}
for _, url := range []string{"http://example.com", "https://user:pass@example.com", "https://example.com?key=secret", "https://example.com#secret", "https://",
"https://example.com:99999/v1", "https://example.com:0/v1", "https://xn--.test", "https://xn--a.test", "https://a..b", "https://999.1.1.1"} {
if (&ModelProviderInput{Protocol: "responses", BaseURL: url, APIKey: "secret"}).Validate() == nil {
t.Fatal("unsafe or unusable provider URL accepted", url)
}
}
for _, url := range []string{"https://example.com:8443/v1", "https://127.0.0.1/v1", "https://[::1]:8443/v1", "https://model_gateway.internal/v1", "https://bücher.example/v1"} {
if err := (&ModelProviderInput{Protocol: "responses", BaseURL: url, APIKey: "secret"}).Validate(); err != nil {
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")
}
Expand Down
59 changes: 0 additions & 59 deletions contracts/agents-api/v1/model_provider_admission.go

This file was deleted.

6 changes: 3 additions & 3 deletions contracts/agents-api/zh/core-errors.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Core 管理错误"
source: contracts/agents-api/core-errors.md
source_hash: 78fcbde855beafae4d1f5eb38b87596eeca25c99c025c6c54978db988d2a534a
source_hash: 50b9624c14d3d600f991fcc9da741b6c4c4722831568c28e849a574bb34a4b06
---

`/core/v1` 上的错误使用此封装结构。`message` 是安全的英文文本;`code` 和 `param` 可以为 null。客户端依据稳定的 `code` 和可选的 `param` 进行处理,对未知代码显示 `message`,绝不解析消息,也绝不自动重试被拒绝的写操作。
Expand Down Expand Up @@ -60,9 +60,9 @@ Web 的控制台服务器在 `/core` 路径上发生自身故障时使用此封
| `invalid_name` | `name` | `max_length`:Projects 和节点为 128,Project 键为 80 | 名称未通过相应资源的验证器 |
| `invalid_node_capacity` | `max_active` 或 `max_retained` | `min`:1,`max`:1000000 | 容量无效;保留容量还必须至少等于活动容量 |
| `invalid_model_provider` | null | 省略 | 必须提供完整的模型提供商配置包 |
| `model_provider_base_url_invalid` | `base_url` | 省略 | 必须使用 HTTPS,且不得包含凭据、查询或片段 |
| `model_provider_base_url_invalid` | `base_url` | 省略 | 基础 URL 不符合[提供商规则](./model-execution.md#session-override) |
| `model_provider_protocol_unsupported` | `protocol` | `harness` 和 `allowed_protocols`,来自该构建的适配器目录 | 协议未知,或所选 Harness 不支持该协议 |
| `model_provider_api_key_invalid` | `api_key` | `max_length`:16384 | 密钥为空、过长或包含禁止字符 |
| `model_provider_api_key_invalid` | `api_key` | `max_length`:16384 | 密钥不符合[提供商规则](./model-execution.md#session-override) |
| `model_provider_token_limits_invalid` | `context_window` 或 `max_output_tokens` | 省略 | 限制无效,或 Harness 要求的正数限制缺失 |
| `model_configuration_model_invalid` | `model` | 省略 | 部署默认配置的 model 不是非空模型标识符 |
| `harness_config_invalid` | `harness_config` | 省略 | 部署默认配置的原生参数不受支持或无效 |
Expand Down
Loading
Loading