iop/packages/go/config/protocol_profile.go
toki e5869a53ae fix(edge): provider 호출 정규화 계층을 추가한다
caller별 예외 대신 요청 의미와 protocol profile capability로 operation을 선택해 tools와 effort 조합을 보존한다. 지원하지 않는 effort는 가장 가까운 하위 등급으로만 내리고 상향 매핑은 거부한다.
2026-08-13 23:06:50 +09:00

866 lines
32 KiB
Go

package config
import (
"fmt"
"net/url"
"strings"
)
// ProtocolDriver identifies the wire/stream semantics of a protocol profile.
type ProtocolDriver string
const (
ProtocolDriverOpenAIChat ProtocolDriver = "openai_chat"
ProtocolDriverAnthropicMessages ProtocolDriver = "anthropic_messages"
ProtocolDriverOpenAIResponses ProtocolDriver = "openai_responses"
)
// validProtocolDrivers is the closed set of admissible driver values.
var validProtocolDrivers = map[ProtocolDriver]struct{}{
ProtocolDriverOpenAIChat: {},
ProtocolDriverAnthropicMessages: {},
ProtocolDriverOpenAIResponses: {},
}
// ProtocolOperation is a named operation within a protocol profile.
type ProtocolOperation string
const (
OperationModels ProtocolOperation = "models"
OperationChatCompletions ProtocolOperation = "chat_completions"
OperationMessages ProtocolOperation = "messages"
OperationCountTokens ProtocolOperation = "count_tokens"
OperationResponses ProtocolOperation = "responses"
)
// validProtocolOperations is the closed set of admissible operation ids.
var validProtocolOperations = map[ProtocolOperation]struct{}{
OperationModels: {},
OperationChatCompletions: {},
OperationMessages: {},
OperationCountTokens: {},
OperationResponses: {},
}
// validOperationsByDriver restricts which operations each driver may declare.
var validOperationsByDriver = map[ProtocolDriver]map[ProtocolOperation]struct{}{
ProtocolDriverOpenAIChat: {
OperationModels: {},
OperationChatCompletions: {},
OperationResponses: {},
OperationCountTokens: {},
},
ProtocolDriverAnthropicMessages: {
OperationModels: {},
OperationMessages: {},
OperationCountTokens: {},
},
ProtocolDriverOpenAIResponses: {
OperationModels: {},
OperationResponses: {},
OperationCountTokens: {},
},
}
// validProtocolCapabilities is the closed vocabulary shared by config,
// admission, and request preparation. Lifecycle capabilities are a separate
// provider concern and intentionally do not participate in this set.
var validProtocolCapabilities = map[string]struct{}{
"models": {},
"chat": {},
"messages": {},
"responses": {},
"streaming": {},
"tool_calling": {},
"count_tokens": {},
}
var operationCapability = map[ProtocolOperation]string{
OperationModels: "models",
OperationChatCompletions: "chat",
OperationMessages: "messages",
OperationCountTokens: "count_tokens",
OperationResponses: "responses",
}
// TokenCounterMode describes how a model group's input tokens are counted.
type TokenCounterMode string
const (
TokenCounterDeterministic TokenCounterMode = "deterministic"
TokenCounterEstimate TokenCounterMode = "estimate"
)
// ProtocolAuthConf declares how a profile authenticates to its upstream.
type ProtocolAuthConf struct {
// Header is the request header name carrying the credential (e.g.
// "Authorization" or "x-api-key").
Header string `mapstructure:"header" yaml:"header,omitempty"`
// Scheme is the auth scheme prefix (e.g. "Bearer"). Empty means the
// raw header value is the credential.
Scheme string `mapstructure:"scheme" yaml:"scheme,omitempty"`
}
// ProtocolEffortMappingConf declares how one provider operation represents
// IOP's normalized reasoning-effort levels. Levels may map multiple IOP grades
// to one provider grade when the provider exposes a smaller scale.
type ProtocolEffortMappingConf struct {
Levels map[string]string `mapstructure:"levels" yaml:"levels,omitempty"`
WithTools bool `mapstructure:"with_tools" yaml:"with_tools,omitempty"`
TokenBudget bool `mapstructure:"token_budget" yaml:"token_budget,omitempty"`
Wire string `mapstructure:"wire" yaml:"wire,omitempty"`
}
const (
ProtocolEffortWireOpenAIChat = "openai_chat"
ProtocolEffortWireOpenAIResponses = "openai_responses"
ProtocolEffortWireAnthropicMessage = "anthropic_messages"
ProtocolEffortWireGeminiChat = "gemini_openai_chat"
)
var validProtocolEffortWires = map[string]struct{}{
ProtocolEffortWireOpenAIChat: {},
ProtocolEffortWireOpenAIResponses: {},
ProtocolEffortWireAnthropicMessage: {},
ProtocolEffortWireGeminiChat: {},
}
// ProtocolNormalizationConf contains provider-wire normalization facts. It is
// intentionally operation-scoped: a provider may support reasoning with tools
// on Responses while rejecting the same semantic request on Chat Completions.
type ProtocolNormalizationConf struct {
Effort map[string]ProtocolEffortMappingConf `mapstructure:"effort" yaml:"effort,omitempty"`
}
// ProtocolProfileConf is the overlayable configuration of a protocol profile.
// It is the source of truth for endpoint, operation paths, auth, and
// capabilities before concrete resolution.
type ProtocolProfileConf struct {
// Base is the optional id of a built-in/custom profile this overlay
// extends. At most one base is allowed; cycles and unknown bases are
// rejected at config load.
Base string `mapstructure:"base" yaml:"base,omitempty"`
// Driver is the protocol driver (openai_chat, anthropic_messages,
// openai_responses).
Driver ProtocolDriver `mapstructure:"driver" yaml:"driver,omitempty"`
// BaseURL is the normalized upstream base URL (no trailing slash).
BaseURL string `mapstructure:"base_url" yaml:"base_url,omitempty"`
// Operations maps operation id to a relative or absolute URL path.
// Absolute URLs are returned unchanged; relative paths are joined once
// to BaseURL.
Operations map[string]string `mapstructure:"operations" yaml:"operations,omitempty"`
// Auth declares the upstream authentication header/scheme.
Auth ProtocolAuthConf `mapstructure:"auth" yaml:"auth,omitempty"`
// Capabilities lists the profile's declared capabilities (e.g.
// "messages", "streaming", "tool_calling").
Capabilities []string `mapstructure:"capabilities" yaml:"capabilities,omitempty"`
// ModelMapping maps selected/canonical model ids to upstream model ids.
ModelMapping map[string]string `mapstructure:"model_mapping" yaml:"model_mapping,omitempty"`
// Extensions holds restricted profile-specific options that cannot be
// expressed in the typed fields above.
Extensions map[string]any `mapstructure:"extensions" yaml:"extensions,omitempty"`
// Normalization maps IOP semantic request controls to provider operations.
Normalization ProtocolNormalizationConf `mapstructure:"normalization" yaml:"normalization,omitempty"`
}
var identityReasoningEffortLevels = map[string]string{
"none": "none", "low": "low", "medium": "medium",
"high": "high", "xhigh": "xhigh", "max": "max",
}
var reasoningEffortOrder = []string{"none", "low", "medium", "high", "xhigh", "max"}
func reasoningEffortGradeIndex(level string) int {
for index, candidate := range reasoningEffortOrder {
if candidate == level {
return index
}
}
return -1
}
func identityEffortMapping(wire string, withTools, tokenBudget bool) ProtocolEffortMappingConf {
levels := make(map[string]string, len(identityReasoningEffortLevels))
for level, providerLevel := range identityReasoningEffortLevels {
levels[level] = providerLevel
}
return ProtocolEffortMappingConf{Levels: levels, WithTools: withTools, TokenBudget: tokenBudget, Wire: wire}
}
// ConcreteProtocolProfile is the immutable, resolved snapshot of a protocol
// profile. It is what gets carried through the wire, Node config, and adapter
// resolution. It never contains a Base reference.
type ConcreteProtocolProfile struct {
ID string
ProtocolProfileConf
}
// TokenCounterConf declares how a model group's input tokens are counted
// without an upstream call.
type TokenCounterConf struct {
// Mode is "deterministic" or "estimate".
Mode TokenCounterMode `mapstructure:"mode" yaml:"mode,omitempty"`
// Per1kInput is the estimated input tokens per 1k characters for
// estimate mode.
Per1kInput int `mapstructure:"per_1k_input" yaml:"per_1k_input,omitempty"`
}
// builtInProtocolProfiles is the process-owned catalog. Config normalization
// always clones from this private map, so callers cannot mutate runtime
// defaults through the exported compatibility snapshot below.
var builtInProtocolProfiles = map[string]ProtocolProfileConf{
"openai": {
Driver: ProtocolDriverOpenAIChat,
BaseURL: "https://api.openai.com/v1",
Operations: map[string]string{
string(OperationModels): "/models",
string(OperationChatCompletions): "/chat/completions",
string(OperationResponses): "/responses",
},
Auth: ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
Capabilities: []string{"models", "chat", "streaming", "tool_calling", "responses"},
Normalization: ProtocolNormalizationConf{Effort: map[string]ProtocolEffortMappingConf{
string(OperationChatCompletions): identityEffortMapping(ProtocolEffortWireOpenAIChat, false, false),
string(OperationResponses): identityEffortMapping(ProtocolEffortWireOpenAIResponses, true, false),
}},
},
"gemini": {
Driver: ProtocolDriverOpenAIChat,
BaseURL: "https://generativelanguage.googleapis.com/v1beta/openai",
Operations: map[string]string{
string(OperationModels): "/models",
string(OperationChatCompletions): "/chat/completions",
},
Auth: ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
Capabilities: []string{"models", "chat", "streaming", "tool_calling"},
Normalization: ProtocolNormalizationConf{Effort: map[string]ProtocolEffortMappingConf{
string(OperationChatCompletions): identityEffortMapping(ProtocolEffortWireGeminiChat, true, true),
}},
},
"anthropic": {
Driver: ProtocolDriverAnthropicMessages,
BaseURL: "https://api.anthropic.com",
Operations: map[string]string{
string(OperationMessages): "/v1/messages",
string(OperationCountTokens): "/v1/messages/count_tokens",
},
Auth: ProtocolAuthConf{Header: "x-api-key"},
Capabilities: []string{"messages", "streaming", "tool_calling", "count_tokens"},
Normalization: ProtocolNormalizationConf{Effort: map[string]ProtocolEffortMappingConf{
string(OperationMessages): identityEffortMapping(ProtocolEffortWireAnthropicMessage, true, true),
}},
},
"glm": {
Driver: ProtocolDriverOpenAIChat,
BaseURL: "https://api.z.ai/api/paas/v4",
Operations: map[string]string{
string(OperationModels): "/models",
string(OperationChatCompletions): "/chat/completions",
},
Auth: ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
Capabilities: []string{"models", "chat", "streaming", "tool_calling"},
},
"glm_coding": {
Driver: ProtocolDriverOpenAIChat,
BaseURL: "https://api.z.ai/api/coding/paas/v4",
Operations: map[string]string{
string(OperationModels): "/models",
string(OperationChatCompletions): "/chat/completions",
},
Auth: ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
Capabilities: []string{"models", "chat", "streaming", "tool_calling"},
},
"kimi": {
Driver: ProtocolDriverOpenAIChat,
BaseURL: "https://api.moonshot.cn/v1",
Operations: map[string]string{
string(OperationModels): "/models",
string(OperationChatCompletions): "/chat/completions",
},
Auth: ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
Capabilities: []string{"models", "chat", "streaming"},
},
"minimax_chat": {
Driver: ProtocolDriverOpenAIChat,
BaseURL: "https://api.minimax.io/v1",
Operations: map[string]string{
string(OperationModels): "/models",
string(OperationChatCompletions): "/chat/completions",
},
Auth: ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
Capabilities: []string{"models", "chat", "streaming"},
},
"minimax_messages": {
Driver: ProtocolDriverAnthropicMessages,
BaseURL: "https://api.minimax.io/anthropic",
Operations: map[string]string{
string(OperationMessages): "/v1/messages",
},
Auth: ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
Capabilities: []string{"messages", "streaming"},
},
"mimo_chat": {
Driver: ProtocolDriverOpenAIChat,
BaseURL: "https://api.xiaomimimo.com/v1",
Operations: map[string]string{
string(OperationModels): "/models",
string(OperationChatCompletions): "/chat/completions",
},
Auth: ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
Capabilities: []string{"models", "chat", "streaming"},
},
"mimo_messages": {
Driver: ProtocolDriverAnthropicMessages,
BaseURL: "https://api.xiaomimimo.com/anthropic",
Operations: map[string]string{
string(OperationMessages): "/v1/messages",
},
Auth: ProtocolAuthConf{Header: "api-key"},
Capabilities: []string{"messages", "streaming"},
},
"grok": {
Driver: ProtocolDriverOpenAIChat,
BaseURL: "https://api.x.ai/v1",
Operations: map[string]string{
string(OperationModels): "/models",
string(OperationChatCompletions): "/chat/completions",
},
Auth: ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
Capabilities: []string{"models", "chat", "streaming"},
},
"seulgi_chat": {
Driver: ProtocolDriverOpenAIChat,
BaseURL: "https://seulgivibe.example.invalid/openai/v1",
Operations: map[string]string{
string(OperationModels): "/models",
string(OperationChatCompletions): "/chat/completions",
},
Auth: ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
Capabilities: []string{"models", "chat", "streaming"},
},
"seulgi_messages": {
Driver: ProtocolDriverAnthropicMessages,
BaseURL: "https://seulgivibe.example.invalid/anthropic/v1",
Operations: map[string]string{
string(OperationMessages): "/messages",
string(OperationCountTokens): "/messages/count_tokens",
},
Auth: ProtocolAuthConf{Header: "x-api-key"},
Capabilities: []string{"messages", "streaming", "tool_calling", "count_tokens"},
},
}
// BuiltInProtocolProfiles is a compatibility snapshot for callers that resolve
// profiles directly. Runtime config assembly uses a fresh private clone via
// BuiltInProtocolProfileCatalog and is therefore immune to caller mutation.
var BuiltInProtocolProfiles = cloneProfileCatalog(builtInProtocolProfiles)
// BuiltInProtocolProfileCatalog returns a fully independent built-in catalog.
func BuiltInProtocolProfileCatalog() map[string]ProtocolProfileConf {
return cloneProfileCatalog(builtInProtocolProfiles)
}
func cloneProfileCatalog(src map[string]ProtocolProfileConf) map[string]ProtocolProfileConf {
dst := make(map[string]ProtocolProfileConf, len(src))
for id, profile := range src {
dst[id] = deepCopyProfileConf(profile)
}
return dst
}
// legacyProviderTypeToProfile maps legacy provider type strings to built-in
// profile ids. This is used only when no explicit profile selector is set.
var legacyProviderTypeToProfile = map[string]string{
"openai_api": "openai",
"vllm": "openai",
"vllm-mlx": "openai",
"lemonade": "openai",
"sglang": "openai",
"openai_compat": "openai",
"seulgivibe_claude": "seulgi_messages",
"seulgivibe_openai": "seulgi_chat",
"anthropic": "anthropic",
}
// LegacyProviderTypeToProfile returns the built-in profile id that a legacy
// provider type normalizes to. Returns "" when no mapping exists.
func LegacyProviderTypeToProfile(providerType string) string {
return legacyProviderTypeToProfile[strings.TrimSpace(providerType)]
}
// validateProfileOperation checks that an operation id is valid for the
// given driver.
func validateProfileOperation(driver ProtocolDriver, op string) error {
ops, ok := validOperationsByDriver[driver]
if !ok {
return fmt.Errorf("profile driver %q is not recognized", driver)
}
opID := ProtocolOperation(op)
if _, valid := validProtocolOperations[opID]; !valid {
return fmt.Errorf("profile operation %q is not a known operation", op)
}
if _, valid := ops[opID]; !valid {
return fmt.Errorf("profile operation %q is not supported for driver %q", op, driver)
}
return nil
}
// validateProfileURL validates that an operation path is a well-formed
// relative path or absolute URL.
func validateProfileURL(baseURL, opPath string) error {
trimmed := strings.TrimSpace(opPath)
if trimmed == "" {
return fmt.Errorf("profile operation path must not be empty")
}
if strings.HasPrefix(trimmed, "http://") || strings.HasPrefix(trimmed, "https://") {
parsed, err := url.Parse(trimmed)
if err != nil {
return fmt.Errorf("profile operation path %q is not a valid absolute URL: %w", trimmed, err)
}
if (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" {
return fmt.Errorf("profile operation path %q must be an absolute http(s) URL", trimmed)
}
return nil
}
if !strings.HasPrefix(trimmed, "/") {
return fmt.Errorf("profile operation path %q must start with '/' for relative paths (leading slash required)", trimmed)
}
if _, err := url.Parse(baseURL); err != nil {
return fmt.Errorf("profile base_url %q is not a valid URL: %w", baseURL, err)
}
return nil
}
// deepCopyProfileConf creates a deep copy of a ProtocolProfileConf, copying
// all maps and slices so mutations to the copy do not affect the original.
func deepCopyProfileConf(src ProtocolProfileConf) ProtocolProfileConf {
dst := src
if src.Operations != nil {
dst.Operations = make(map[string]string, len(src.Operations))
for k, v := range src.Operations {
dst.Operations[k] = v
}
}
if src.Capabilities != nil {
dst.Capabilities = append([]string(nil), src.Capabilities...)
}
if src.ModelMapping != nil {
dst.ModelMapping = make(map[string]string, len(src.ModelMapping))
for k, v := range src.ModelMapping {
dst.ModelMapping[k] = v
}
}
if src.Extensions != nil {
dst.Extensions = deepCopyExtensions(src.Extensions)
}
if src.Normalization.Effort != nil {
dst.Normalization.Effort = make(map[string]ProtocolEffortMappingConf, len(src.Normalization.Effort))
for operation, mapping := range src.Normalization.Effort {
dst.Normalization.Effort[operation] = cloneEffortMapping(mapping)
}
}
return dst
}
func cloneEffortMapping(src ProtocolEffortMappingConf) ProtocolEffortMappingConf {
dst := ProtocolEffortMappingConf{
WithTools: src.WithTools,
TokenBudget: src.TokenBudget,
Wire: src.Wire,
}
if src.Levels != nil {
dst.Levels = make(map[string]string, len(src.Levels))
for level, providerLevel := range src.Levels {
dst.Levels[level] = providerLevel
}
}
return dst
}
// deepCopyExtensions deep-copies a map[string]any, handling nested maps and
// slices.
func deepCopyExtensions(src map[string]any) map[string]any {
if src == nil {
return nil
}
dst := make(map[string]any, len(src))
for k, v := range src {
dst[k] = deepCopyAny(v)
}
return dst
}
func deepCopyAny(v any) any {
switch val := v.(type) {
case map[string]any:
return deepCopyExtensions(val)
case map[string]string:
out := make(map[string]string, len(val))
for k, item := range val {
out[k] = item
}
return out
case []any:
out := make([]any, len(val))
for i, item := range val {
out[i] = deepCopyAny(item)
}
return out
case []string:
return append([]string(nil), val...)
default:
return v
}
}
// ResolveProtocolProfile resolves a provider's profile selector against the
// catalog (built-in + custom overlays), producing an immutable
// ConcreteProtocolProfile snapshot. It enforces:
// - cycle detection (base chain must terminate)
// - unknown base rejection
// - driver conflict (overlay driver must match base driver)
// - operation/auth/capability validation
// - URL validation
//
// When selector is empty, legacyType is consulted for alias normalization.
// If neither resolves, the returned error explains the failure.
func ResolveProtocolProfile(selector, legacyType string, catalog map[string]ProtocolProfileConf) (ConcreteProtocolProfile, error) {
id := strings.TrimSpace(selector)
if id == "" {
id = LegacyProviderTypeToProfile(legacyType)
}
if id == "" {
return ConcreteProtocolProfile{}, fmt.Errorf("provider profile: no profile selector and no legacy type mapping for %q", legacyType)
}
visited := make(map[string]struct{})
var resolve func(pid string) (ProtocolProfileConf, error)
resolve = func(pid string) (ProtocolProfileConf, error) {
if _, cycle := visited[pid]; cycle {
return ProtocolProfileConf{}, fmt.Errorf("profile %q: base cycle detected", pid)
}
visited[pid] = struct{}{}
base, ok := catalog[pid]
if !ok {
return ProtocolProfileConf{}, fmt.Errorf("profile %q: unknown profile id", pid)
}
if strings.TrimSpace(base.Base) != "" {
parent, err := resolve(base.Base)
if err != nil {
return ProtocolProfileConf{}, err
}
merged := deepCopyProfileConf(parent)
overlay := deepCopyProfileConf(base)
merged, err = mergeProfileOverlay(merged, overlay)
if err != nil {
return ProtocolProfileConf{}, err
}
return merged, nil
}
return deepCopyProfileConf(base), nil
}
resolved, err := resolve(id)
if err != nil {
return ConcreteProtocolProfile{}, err
}
if err := validateConcreteProfile(id, resolved); err != nil {
return ConcreteProtocolProfile{}, err
}
return ConcreteProtocolProfile{ID: id, ProtocolProfileConf: resolved}, nil
}
// mergeProfileOverlay merges an overlay onto a base profile. Overlay fields
// that are non-zero/non-empty replace base fields; nil/empty overlay fields
// inherit from the base. A driver conflict (overlay driver differs from base
// driver) returns an error.
func mergeProfileOverlay(base, overlay ProtocolProfileConf) (ProtocolProfileConf, error) {
merged := base
if strings.TrimSpace(string(overlay.Driver)) != "" {
if overlay.Driver != base.Driver {
return ProtocolProfileConf{}, fmt.Errorf("profile: driver conflict: base %q vs overlay %q", base.Driver, overlay.Driver)
}
}
if strings.TrimSpace(overlay.BaseURL) != "" {
merged.BaseURL = overlay.BaseURL
}
if len(overlay.Operations) > 0 {
if merged.Operations == nil {
merged.Operations = make(map[string]string)
}
for k, v := range overlay.Operations {
merged.Operations[k] = v
}
}
if overlay.Auth.Header != "" {
merged.Auth.Header = overlay.Auth.Header
}
if overlay.Auth.Scheme != "" {
merged.Auth.Scheme = overlay.Auth.Scheme
}
if len(overlay.Capabilities) > 0 {
merged.Capabilities = overlay.Capabilities
}
if len(overlay.ModelMapping) > 0 {
if merged.ModelMapping == nil {
merged.ModelMapping = make(map[string]string)
}
for k, v := range overlay.ModelMapping {
merged.ModelMapping[k] = v
}
}
if len(overlay.Extensions) > 0 {
if merged.Extensions == nil {
merged.Extensions = make(map[string]any)
}
for k, v := range overlay.Extensions {
merged.Extensions[k] = v
}
}
if len(overlay.Normalization.Effort) > 0 {
if merged.Normalization.Effort == nil {
merged.Normalization.Effort = make(map[string]ProtocolEffortMappingConf)
}
for operation, mapping := range overlay.Normalization.Effort {
merged.Normalization.Effort[operation] = cloneEffortMapping(mapping)
}
}
return merged, nil
}
// validateConcreteProfile validates a resolved concrete profile: driver is
// known, operations are valid for the driver, URLs are well-formed, and
// capabilities are non-empty.
func validateConcreteProfile(id string, p ProtocolProfileConf) error {
if _, ok := validProtocolDrivers[p.Driver]; !ok {
return fmt.Errorf("profile %q: driver %q is not one of openai_chat/anthropic_messages/openai_responses", id, p.Driver)
}
if strings.TrimSpace(p.BaseURL) == "" {
return fmt.Errorf("profile %q: base_url must not be empty", id)
}
base, err := url.Parse(p.BaseURL)
if err != nil {
return fmt.Errorf("profile %q: base_url %q is not a valid URL: %w", id, p.BaseURL, err)
}
if (base.Scheme != "http" && base.Scheme != "https") || base.Host == "" {
return fmt.Errorf("profile %q: base_url %q must be an absolute http(s) URL", id, p.BaseURL)
}
if len(p.Operations) == 0 {
return fmt.Errorf("profile %q: operations must not be empty", id)
}
if len(p.Capabilities) == 0 {
return fmt.Errorf("profile %q: capabilities must not be empty", id)
}
capabilities := make(map[string]struct{}, len(p.Capabilities))
for _, capability := range p.Capabilities {
capability = strings.TrimSpace(capability)
if _, ok := validProtocolCapabilities[capability]; !ok {
return fmt.Errorf("profile %q: capability %q is not recognized", id, capability)
}
if _, duplicate := capabilities[capability]; duplicate {
return fmt.Errorf("profile %q: capability %q is duplicated", id, capability)
}
capabilities[capability] = struct{}{}
}
for op, path := range p.Operations {
if err := validateProfileOperation(p.Driver, op); err != nil {
return fmt.Errorf("profile %q: %w", id, err)
}
if err := validateProfileURL(p.BaseURL, path); err != nil {
return fmt.Errorf("profile %q: operation %q: %w", id, op, err)
}
if required := operationCapability[ProtocolOperation(op)]; required != "" {
if _, ok := capabilities[required]; !ok {
return fmt.Errorf("profile %q: operation %q requires capability %q", id, op, required)
}
}
}
for operation, mapping := range p.Normalization.Effort {
if _, ok := p.Operations[operation]; !ok {
return fmt.Errorf("profile %q: effort normalization operation %q is not declared", id, operation)
}
if len(mapping.Levels) == 0 {
return fmt.Errorf("profile %q: effort normalization operation %q has no levels", id, operation)
}
if _, ok := validProtocolEffortWires[mapping.Wire]; !ok {
return fmt.Errorf("profile %q: effort normalization operation %q has invalid wire %q", id, operation, mapping.Wire)
}
for level, providerLevel := range mapping.Levels {
levelIndex := reasoningEffortGradeIndex(level)
if levelIndex < 0 {
return fmt.Errorf("profile %q: effort normalization level %q is not recognized", id, level)
}
providerLevel = strings.TrimSpace(providerLevel)
if providerLevel == "" {
return fmt.Errorf("profile %q: effort normalization level %q has an empty provider value", id, level)
}
// Canonical provider grades may collapse downward, but an explicit
// mapping must never silently upgrade the caller's requested grade.
if providerIndex := reasoningEffortGradeIndex(providerLevel); providerIndex > levelIndex {
return fmt.Errorf("profile %q: effort normalization level %q upgrades to %q", id, level, providerLevel)
}
}
}
if p.Auth.Header == "" {
return fmt.Errorf("profile %q: auth.header must not be empty", id)
}
if !validHTTPFieldName(p.Auth.Header) {
return fmt.Errorf("profile %q: auth.header %q is not a valid HTTP header name", id, p.Auth.Header)
}
authHeader := strings.ToLower(p.Auth.Header)
authScheme := strings.TrimSpace(p.Auth.Scheme)
if authHeader == "authorization" && !strings.EqualFold(authScheme, "Bearer") {
return fmt.Errorf("profile %q: Authorization auth requires Bearer scheme", id)
}
if authHeader != "authorization" && authScheme != "" {
return fmt.Errorf("profile %q: auth header %q must use a raw credential without scheme", id, p.Auth.Header)
}
switch p.Driver {
case ProtocolDriverOpenAIChat:
if _, ok := capabilities["chat"]; !ok {
return fmt.Errorf("profile %q: driver %q requires capability %q", id, p.Driver, "chat")
}
if _, ok := p.Operations[string(OperationChatCompletions)]; !ok {
return fmt.Errorf("profile %q: driver %q requires operation %q", id, p.Driver, OperationChatCompletions)
}
case ProtocolDriverAnthropicMessages:
if _, ok := capabilities["messages"]; !ok {
return fmt.Errorf("profile %q: driver %q requires capability %q", id, p.Driver, "messages")
}
if _, ok := p.Operations[string(OperationMessages)]; !ok {
return fmt.Errorf("profile %q: driver %q requires operation %q", id, p.Driver, OperationMessages)
}
case ProtocolDriverOpenAIResponses:
if _, ok := capabilities["responses"]; !ok {
return fmt.Errorf("profile %q: driver %q requires capability %q", id, p.Driver, "responses")
}
if _, ok := p.Operations[string(OperationResponses)]; !ok {
return fmt.Errorf("profile %q: driver %q requires operation %q", id, p.Driver, OperationResponses)
}
}
return nil
}
// validHTTPFieldName implements the RFC 9110 field-name token grammar. It is
// deliberately byte-oriented because HTTP field names are ASCII-only tokens.
func validHTTPFieldName(name string) bool {
if name == "" {
return false
}
for i := 0; i < len(name); i++ {
c := name[i]
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') {
continue
}
switch c {
case '!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '_', '`', '|', '~':
continue
default:
return false
}
}
return true
}
// ResolveOperationURL resolves an operation id to its full URL. Absolute
// operation URLs are returned unchanged; relative paths are joined once to
// the normalized base URL. Missing operations return a typed error.
func (p ConcreteProtocolProfile) ResolveOperationURL(op string) (string, error) {
if strings.TrimSpace(op) == "" {
return "", fmt.Errorf("protocol profile %q: operation must not be empty", p.ID)
}
if err := validateProfileOperation(p.Driver, op); err != nil {
return "", fmt.Errorf("protocol profile %q: %w", p.ID, err)
}
path, ok := p.Operations[op]
if !ok {
return "", fmt.Errorf("protocol profile %q: operation %q is not supported", p.ID, op)
}
return resolveOperationURL(p.BaseURL, path)
}
// resolveOperationURL joins a base URL and operation path exactly once.
func resolveOperationURL(baseURL, opPath string) (string, error) {
trimmed := strings.TrimSpace(opPath)
if trimmed == "" {
return "", fmt.Errorf("operation path must not be empty")
}
if strings.HasPrefix(trimmed, "http://") || strings.HasPrefix(trimmed, "https://") {
return trimmed, nil
}
base, err := url.Parse(baseURL)
if err != nil {
return "", fmt.Errorf("base_url %q is not a valid URL: %w", baseURL, err)
}
operation, err := url.Parse(trimmed)
if err != nil {
return "", fmt.Errorf("operation path %q is not a valid URL path: %w", opPath, err)
}
basePath := strings.TrimRight(base.Path, "/")
base.Path = basePath + operation.Path
base.RawQuery = operation.RawQuery
base.Fragment = operation.Fragment
return base.String(), nil
}
// HasCapability reports whether the profile declares a capability.
func (p ConcreteProtocolProfile) HasCapability(cap string) bool {
for _, c := range p.Capabilities {
if c == cap {
return true
}
}
return false
}
// MapModel returns the upstream model id for a selected/canonical model. An
// absent mapping is an identity mapping.
func (p ConcreteProtocolProfile) MapModel(model string) string {
if mapped := strings.TrimSpace(p.ModelMapping[model]); mapped != "" {
return mapped
}
return model
}
// MapReasoningEffort maps one normalized IOP effort grade to the provider
// value for an operation. If the exact grade is unsupported, it selects the
// nearest explicitly supported lower grade. It never upgrades effort and
// rejects unsupported operation/tool combinations.
func (p ConcreteProtocolProfile) MapReasoningEffort(operation ProtocolOperation, level string, hasTools bool) (string, bool) {
mapping, ok := p.Normalization.Effort[string(operation)]
if !ok || (hasTools && !mapping.WithTools) {
return "", false
}
requestedIndex := reasoningEffortGradeIndex(level)
if requestedIndex < 0 {
return "", false
}
for index := requestedIndex; index >= 0; index-- {
mapped := strings.TrimSpace(mapping.Levels[reasoningEffortOrder[index]])
if mapped != "" {
return mapped, true
}
}
return "", false
}
// EffortMapping returns an operation-scoped copy of the normalization facts.
func (p ConcreteProtocolProfile) EffortMapping(operation ProtocolOperation) (ProtocolEffortMappingConf, bool) {
mapping, ok := p.Normalization.Effort[string(operation)]
if !ok {
return ProtocolEffortMappingConf{}, false
}
return cloneEffortMapping(mapping), true
}
// Clone returns a deep copy of the concrete profile.
func (p ConcreteProtocolProfile) Clone() ConcreteProtocolProfile {
return ConcreteProtocolProfile{
ID: p.ID,
ProtocolProfileConf: deepCopyProfileConf(p.ProtocolProfileConf),
}
}