Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

196 lines
6.8 KiB
Go

package runtimeprep
import (
"net/url"
"strconv"
"strings"
)
// ModelEndpointConfig injects a managed model access plan endpoint into a
// provider runtime for one session. The credential travels only through the
// session process environment or the session-scoped provider config; it must
// never be logged, recorded in manifests, or echoed into instructions.
type ModelEndpointConfig struct {
PlanID string
PlanName string
// Protocol is the plan wire protocol family: "openai" or "anthropic".
Protocol string
BaseURL string
APIKey string
// WireAPI is the provider-facing endpoint shape. Codex accepts "chat" or
// "responses"; daemon gateways set "responses" while legacy/direct
// callers retain "chat" when this field is empty.
WireAPI string
// Model is the default model id for the session; providers may still
// switch models within the plan on later calls.
Model string
// Models lists every model the plan authorizes (redaction-safe ids and
// display names). Providers that materialize a session-scoped catalog
// (OpenCode's provider block) need the full list, not just the default.
Models []ModelEndpointModel
PlanUpdatedAtUnixMS int64
}
// ModelEndpointModel is one selectable model exposed by the bound plan.
type ModelEndpointModel struct {
ID string
Name string
}
// ModelPlanProviderID names the provider entry injected into a session-scoped
// provider config (Codex `[model_providers.*]` table, OpenCode `provider`
// block). The service layer namespaces OpenCode composer model values with the
// same id, so the two sides must never drift.
const ModelPlanProviderID = "tutti-model-plan"
// codexModelPlanProviderID names the injected Codex model provider entry.
const codexModelPlanProviderID = ModelPlanProviderID
// ModelPlanAPIKeyEnv carries the plan credential into the provider process;
// session-scoped configs reference it instead of embedding the key.
const ModelPlanAPIKeyEnv = "TUTTI_MODEL_PLAN_API_KEY"
// codexModelPlanAPIKeyEnv carries the plan credential into the Codex process.
const codexModelPlanAPIKeyEnv = ModelPlanAPIKeyEnv
func (c *ModelEndpointConfig) valid() bool {
return c != nil && strings.TrimSpace(c.BaseURL) != "" && strings.TrimSpace(c.APIKey) != ""
}
func (c *ModelEndpointConfig) supportsCodex() bool {
return c.valid() && strings.TrimSpace(c.Protocol) == "openai"
}
func (c *ModelEndpointConfig) supportsClaudeCode() bool {
return c.valid() && strings.TrimSpace(c.Protocol) == "anthropic"
}
func (c *ModelEndpointConfig) supportsOpenCode() bool {
return c.valid() && strings.TrimSpace(c.Protocol) == "openai"
}
// OpenCodePlanModelValue renders a plan model id in OpenCode's
// "<provider>/<model>" addressing, rooted at the injected provider entry.
// Already-namespaced values pass through unchanged.
func OpenCodePlanModelValue(modelID string) string {
modelID = strings.TrimSpace(modelID)
if modelID == "" || strings.HasPrefix(modelID, ModelPlanProviderID+"/") {
return modelID
}
return ModelPlanProviderID + "/" + modelID
}
// OpenCodePlanModelID strips the injected-provider namespace from an OpenCode
// composer model value, returning the plan-domain model id.
func OpenCodePlanModelID(value string) string {
return strings.TrimPrefix(strings.TrimSpace(value), ModelPlanProviderID+"/")
}
// modelEndpointClaudeEnv maps an anthropic-protocol plan endpoint onto the
// Claude Code environment contract. Anthropic's official endpoint and Kimi
// Coding authenticate with ANTHROPIC_API_KEY (x-api-key); relays and other
// coding-plan gateways generally expect the bearer ANTHROPIC_AUTH_TOKEN form.
func modelEndpointClaudeEnv(endpoint *ModelEndpointConfig) []string {
if !endpoint.supportsClaudeCode() {
return nil
}
baseURL := strings.TrimSpace(endpoint.BaseURL)
env := []string{"ANTHROPIC_BASE_URL=" + strings.TrimSuffix(baseURL, "/v1")}
if modelEndpointUsesAnthropicAPIKey(baseURL) {
env = append(
env,
"ANTHROPIC_API_KEY="+endpoint.APIKey,
"ANTHROPIC_AUTH_TOKEN=",
)
} else {
env = append(
env,
"ANTHROPIC_AUTH_TOKEN="+endpoint.APIKey,
"ANTHROPIC_API_KEY=",
)
}
return env
}
func modelEndpointUsesAnthropicAPIKey(baseURL string) bool {
parsed, err := url.Parse(baseURL)
if err != nil {
return false
}
switch strings.ToLower(parsed.Hostname()) {
case "api.anthropic.com", "api.kimi.com":
return true
default:
return false
}
}
// codexConfigWithModelPlanEndpoint rewrites the session-scoped Codex
// config.toml to route through the injected plan endpoint: it pins the
// top-level model_provider (and default model when supplied) and appends the
// provider table. The API key stays out of the file; Codex reads it from the
// session env via env_key.
func codexConfigWithModelPlanEndpoint(content string, endpoint *ModelEndpointConfig) (string, bool) {
if !endpoint.supportsCodex() {
return content, false
}
next := codexConfigWithTopLevelAssignment(content, "model_provider", strconv.Quote(codexModelPlanProviderID))
if model := strings.TrimSpace(endpoint.Model); model != "" {
next = codexConfigWithTopLevelAssignment(next, "model", strconv.Quote(model))
}
table := "[model_providers." + codexModelPlanProviderID + "]\n" +
"name = " + strconv.Quote(planProviderDisplayName(endpoint)) + "\n" +
"base_url = " + strconv.Quote(strings.TrimSpace(endpoint.BaseURL)) + "\n" +
"env_key = " + strconv.Quote(codexModelPlanAPIKeyEnv) + "\n" +
"wire_api = " + strconv.Quote(codexModelEndpointWireAPI(endpoint)) + "\n"
if !strings.Contains(next, "[model_providers."+codexModelPlanProviderID+"]") {
if strings.TrimSpace(next) == "" {
next = table
} else {
next = strings.TrimRight(next, "\r\n") + "\n\n" + table
}
}
return next, next != content
}
func codexModelEndpointWireAPI(endpoint *ModelEndpointConfig) string {
if endpoint != nil && strings.TrimSpace(endpoint.WireAPI) == "responses" {
return "responses"
}
return "chat"
}
func planProviderDisplayName(endpoint *ModelEndpointConfig) string {
if name := strings.TrimSpace(endpoint.PlanName); name != "" {
return name
}
return "Tutti Model Plan"
}
// codexConfigWithTopLevelAssignment replaces the first top-level assignment of
// key before any table header, or prepends the assignment when the key is
// absent.
func codexConfigWithTopLevelAssignment(content string, key string, encodedValue string) string {
line := key + " = " + encodedValue
lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
for index, existingLine := range lines {
trimmed := strings.TrimSpace(existingLine)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
if strings.HasPrefix(trimmed, "[") {
break
}
if !codexConfigLineHasKey(trimmed, key) {
continue
}
nextLines := append([]string{}, lines...)
nextLines[index] = line
return strings.Join(nextLines, "\n")
}
if strings.TrimSpace(content) == "" {
return line + "\n"
}
return line + "\n" + strings.TrimLeft(content, "\r\n")
}