Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
13 KiB
JSON
1 line
13 KiB
JSON
{"repo": "abowloflrf/apid", "files": {"config.example.toml": {"1705ab747e64897248e1baef0a982d94f27f5c47": {"t": 1786067832.0425885, "n": 4452, "h": "e8a4c0480da02e8d", "c": "# apid forwarding config example.\n# Copy to config.toml (or pass --config <path>) and edit.\n# Ops params (listen addr / SQLite / TRACE) stay in env vars; see CLAUDE.md.\n#\n# Two tables:\n# [[upstream]] — a backend, defined once and referenced by name.\n# [[route]] — a public entrypoint (path + client protocol) that dispatches\n# by the request's model to one upstream.\n#\n# Conversion happens when a route's input_protocol differs from the matched\n# upstream's protocol (currently only openai_responses -> openai_chat_completions).\n# Equal protocols = pure forward (bytes passed through, metrics still collected).\n# An upstream with protocol = \"openai_chat_completions\" and\n# supports_responses = true also serves OpenAI Responses natively: responses\n# routes hitting it forward raw bytes to its responses endpoint instead of\n# converting — no duplicate upstream needed for the same provider.\n# Protocols: openai_responses, openai_chat_completions, anthropic_messages.\n#\n# Optional inbound client auth for apid itself. Empty/omitted = disabled.\n# When set, clients must present this key as Authorization: Bearer <key>,\n# X-Api-Key: <key>, or Api-Key: <key>. This key is never forwarded upstream.\n# Only forwarding routes require it; /healthz and /stats(/*) stay open.\n# client_api_key = \"apid-local-static-key\"\n\n# ── Upstreams: write each backend once, reuse from any entrypoint. ──\n[[upstream]]\nname = \"deepseek\"\nprotocol = \"openai_chat_completions\"\nbase_url = \"https://api.deepseek.com/v1\"\npath = \"/chat/completions\"\napi_key = \"sk-xxx\" # empty = pass through the client's Authorization\nmodel = \"deepseek-chat\" # empty = pass through the client's model\n\n[[upstream]]\nname = \"openai\"\nprotocol = \"openai_chat_completions\"\nbase_url = \"https://api.openai.com\"\npath = \"/v1/chat/completions\"\nsupports_responses = true # same backend also speaks Responses natively\n# responses_path = \"/v1/responses\" # optional; empty = derived from path\n# # (strip \"/chat/completions\", append \"/responses\")\n\n[[upstream]]\nname = \"anthropic\"\nprotocol = \"anthropic_messages\"\nbase_url = \"https://api.anthropic.com\"\npath = \"/v1/messages\"\napi_key = \"sk-ant-xxx\" # sent as X-Api-Key for anthropic_messages\n\n# ── Responses entrypoint: clients speak Responses API, dispatched by model. ──\n[[route]]\npath = \"/v1/responses\"\ninput_protocol = \"openai_responses\"\n\n [[route.model]]\n match = \"deepseek-*\" # exact name or glob\n upstream = \"deepseek\" # convert (responses -> chat) since protocols differ\n # model omitted => inherit upstream.model (\"deepseek-chat\")\n\n [[route.model]]\n match = \"gpt-4o\" # rewrite this incoming model for the same upstream\n upstream = \"deepseek\"\n model = \"deepseek-reasoner\" # per-rule override beats upstream.model\n\n [[route.model]]\n match = \"*\" # empty or \"*\" = catch-all (at most one)\n upstream = \"openai\" # supports_responses = true => native responses -> responses\n # passthrough (no conversion), forwarded to /v1/responses\n model = \"\" # \"\" = force pass-through the client's model\n # (omit the key instead to inherit upstream.model)\n\n# ── Chat entrypoint: same upstreams reused, no duplication of backend info. ──\n[[route]]\npath = \"/v1/chat/completions\"\ninput_protocol = \"openai_chat_completions\"\n\n [[route.model]]\n upstream = \"openai\" # equal protocols => pure forward\n\n# ── Anthropic Messages entrypoint: pure forward only, no protocol conversion. ──\n[[route]]\npath = \"/v1/messages\"\ninput_protocol = \"anthropic_messages\"\n\n [[route.model]]\n match = \"claude-*\"\n upstream = \"anthropic\" # equal protocols => pure forward\n\n# ── Standalone web search (optional) ──────────────────────────────────────────\n# When [search] is configured, apid serves POST /v1/alpha/search, the endpoint\n# Codex calls when a custom provider opts into supports_standalone_web_search.\n# This lets Codex's built-in web.run tool work with non-OpenAI backends.\n#\n# Enable on the Codex side by adding to ~/.codex/config.toml:\n# [model_providers.apid]\n# supports_standalone_web_search = true\n#\n# [search]\n# provider = \"exa\" # only \"exa\" supported\n# api_key = \"your-exa-api-key\" # Exa API key (https://exa.ai)\n# # base_url = \"https://api.exa.ai\" # optional; defaults to this\n# # path = \"/v1/alpha/search\" # optional; defaults to this\n"}}, "server/topology_test.go": {"78fe236bdc5b0f2f09211105055787ec25565245": {"t": 1786068476.6464996, "n": 7088, "h": "e5dbdbce9c626335", "c": "package server\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/abowloflrf/apid/config\"\n\t\"github.com/abowloflrf/apid/store\"\n)\n\nfunc ptr(s string) *string { return &s }\n\nfunc topologyConfig() config.Config {\n\treturn config.Config{\n\t\tListen: \":19092\",\n\t\tUpstreams: []config.Upstream{\n\t\t\t{Name: \"deepseek\", Protocol: config.ProtoChat, BaseURL: \"https://api.deepseek.com/v1\", Path: \"/chat/completions\", APIKey: \"sk-secret-chat\", Model: \"deepseek-chat\"},\n\t\t\t{Name: \"anthropic\", Protocol: config.ProtoAnthropic, BaseURL: \"https://api.anthropic.com\", Path: \"/v1/messages\", APIKey: \"sk-secret-ant\"},\n\t\t},\n\t\tRoutes: []config.Route{\n\t\t\t{Path: \"/v1/responses\", InputProtocol: config.ProtoResponses, Models: []config.ModelRule{\n\t\t\t\t{Match: \"gpt-4o\", Upstream: \"deepseek\", Model: ptr(\"deepseek-reasoner\")},\n\t\t\t\t{Match: \"ds-*\", Upstream: \"deepseek\"},\n\t\t\t\t{Match: \"*\", Upstream: \"deepseek\", Model: ptr(\"\")},\n\t\t\t}},\n\t\t\t{Path: \"/v1/messages\", InputProtocol: config.ProtoAnthropic, Models: []config.ModelRule{\n\t\t\t\t{Match: \"claude-*\", Upstream: \"anthropic\"},\n\t\t\t}},\n\t\t},\n\t}\n}\n\nfunc TestTopology(t *testing.T) {\n\ttop := New(topologyConfig(), &store.Store{}, nil).topology()\n\n\tif len(top.Routes) != 2 || len(top.Upstreams) != 2 {\n\t\tt.Fatalf(\"got %d routes / %d upstreams, want 2 / 2\", len(top.Routes), len(top.Upstreams))\n\t}\n\tif top.Storage || top.ClientAuth || top.Search != nil {\n\t\tt.Errorf(\"storage/auth/search should be off: %+v\", top)\n\t}\n\n\trules := top.Routes[0].Rules\n\tif len(rules) != 3 {\n\t\tt.Fatalf(\"got %d rules, want 3\", len(rules))\n\t}\n\t// responses -> chat is a conversion; the per-rule model wins over the upstream's.\n\tif rules[0].Mode != \"convert\" || rules[0].MatchKind != \"exact\" ||\n\t\trules[0].ModelSource != \"rule\" || rules[0].EffectiveModel != \"deepseek-reasoner\" {\n\t\tt.Errorf(\"exact rule = %+v\", rules[0])\n\t}\n\t// Key omitted => inherit the upstream's model.\n\tif rules[1].MatchKind != \"glob\" || rules[1].ModelSource != \"upstream\" || rules[1].EffectiveModel != \"deepseek-chat\" {\n\t\tt.Errorf(\"glob rule = %+v\", rules[1])\n\t}\n\t// model = \"\" forces the client's model through even though the upstream sets one.\n\tif rules[2].MatchKind != \"catchall\" || rules[2].ModelSource != \"passthrough\" || rules[2].EffectiveModel != \"\" {\n\t\tt.Errorf(\"catch-all rule = %+v\", rules[2])\n\t}\n\tif r := top.Routes[1].Rules[0]; r.Mode != \"forward\" {\n\t\tt.Errorf(\"same-protocol route mode = %q, want forward\", r.Mode)\n\t}\n\n\tds := top.Upstreams[0]\n\tif ds.RefCount != 3 || ds.Endpoint != \"https://api.deepseek.com/v1/chat/completions\" {\n\t\tt.Errorf(\"deepseek upstream = %+v\", ds)\n\t}\n\tif ds.Auth != \"api_key\" || ds.AuthHdr != \"Authorization: Bearer\" {\n\t\tt.Errorf(\"chat auth = %q / %q\", ds.Auth, ds.AuthHdr)\n\t}\n\tif ant := top.Upstreams[1]; ant.AuthHdr != \"X-Api-Key\" {\n\t\tt.Errorf(\"anthropic auth header = %q, want X-Api-Key\", ant.AuthHdr)\n\t}\n}\n\n// TestTopologyDualProtocol: supports_responses 的 upstream 在 responses 入口下\n// 显示为 raw forward(via_responses),端点换成 responses 端点。\nfunc TestTopologyDualProtocol(t *testing.T) {\n\tcfg := config.Config{\n\t\tUpstreams: []config.Upstream{{\n\t\t\tName: \"up\",\n\t\t\tProtocol: config.ProtoChat,\n\t\t\tBaseURL: \"https://api.example.com\",\n\t\t\tPath: \"/v1/chat/completions\",\n\t\t\tSupportsResponses: true,\n\t\t}},\n\t\tRoutes: []config.Route{{\n\t\t\tPath: \"/v1/responses\",\n\t\t\tInputProtocol: config.ProtoResponses,\n\t\t\tModels: []config.ModelRule{{Upstream: \"up\"}},\n\t\t}},\n\t}\n\ttop := New(cfg, &store.Store{}, nil).topology()\n\n\tr := top.Routes[0].Rules[0]\n\tif r.Mode != \"forward\" || !r.ViaResponses || r.UpstreamProto != string(config.ProtoResponses) {\n\t\tt.Errorf(\"rule = %+v, want forward via responses with wire protocol openai_responses\", r)\n\t}\n\tif r.Endpoint != \"https://api.example.com/v1/responses\" {\n\t\tt.Errorf(\"rule endpoint = %q, want responses endpoint\", r.Endpoint)\n\t}\n\n\tu := top.Upstreams[0]\n\tif !u.SupportsResponses || u.ResponsesEndpoint != \"https://api.example.com/v1/responses\" {\n\t\tt.Errorf(\"upstream = %+v, want supports_responses with responses endpoint\", u)\n\t}\n}\n\n// Upstream keys must never reach the browser; only the mode they imply.\nfunc TestTopologyHidesSecrets(t *testing.T) {\n\tcfg := topologyConfig()\n\tcfg.ClientAPIKey = \"apid-inbound-key\"\n\tcfg.Search = config.SearchConfig{Provider: \"exa\", APIKey: \"exa-secret\"}\n\n\tw := httptest.NewRecorder()\n\tNew(cfg, &store.Store{}, nil).Handler().ServeHTTP(w, httptest.NewRequest(\"GET\", \"/stats/topology\", nil))\n\tif w.Code != http.StatusOK {\n\t\tt.Fatalf(\"status = %d, want 200\", w.Code)\n\t}\n\tbody := w.Body.String()\n\tfor _, secret := range []string{\"sk-secret-chat\", \"sk-secret-ant\", \"apid-inbound-key\", \"exa-secret\"} {\n\t\tif strings.Contains(body, secret) {\n\t\t\tt.Errorf(\"response leaks %q: %s\", secret, body)\n\t\t}\n\t}\n\n\tvar top topologyResponse\n\tif err := json.Unmarshal(w.Body.Bytes(), &top); err != nil {\n\t\tt.Fatalf(\"decode: %v\", err)\n\t}\n\tif !top.ClientAuth || top.Search == nil || top.Search.Path != \"/v1/alpha/search\" {\n\t\tt.Errorf(\"client auth / search summary = %+v, %+v\", top.ClientAuth, top.Search)\n\t}\n}\n\n// Client auth strips inbound credentials before forwarding, so an upstream with\n// no key of its own ends up unauthenticated — the UI needs to be able to say so.\nfunc TestTopologyAuthStripped(t *testing.T) {\n\tcfg := config.Config{\n\t\tClientAPIKey: \"inbound\",\n\t\tUpstreams: []config.Upstream{{Name: \"u\", Protocol: config.ProtoChat, BaseURL: \"https://x\", Path: \"/v1/chat/completions\"}},\n\t\tRoutes: []config.Route{{Path: \"/v1/chat/completions\", InputProtocol: config.ProtoChat, Models: []config.ModelRule{{Upstream: \"u\"}}}},\n\t}\n\tif got := New(cfg, &store.Store{}, nil).topology().Upstreams[0].Auth; got != \"stripped\" {\n\t\tt.Errorf(\"auth = %q, want stripped\", got)\n\t}\n\tcfg.ClientAPIKey = \"\"\n\tif got := New(cfg, &store.Store{}, nil).topology().Upstreams[0].Auth; got != \"passthrough\" {\n\t\tt.Errorf(\"auth = %q, want passthrough\", got)\n\t}\n}\n\n// The config view reads config, not the DB, so it stays available with storage\n// off — unlike every other /stats/* endpoint.\nfunc TestTopologyWorksWithoutStorage(t *testing.T) {\n\tw := httptest.NewRecorder()\n\tNew(topologyConfig(), &store.Store{}, nil).Handler().ServeHTTP(w, httptest.NewRequest(\"GET\", \"/stats/topology\", nil))\n\tif w.Code != http.StatusOK {\n\t\tt.Fatalf(\"status = %d, want 200; body=%s\", w.Code, w.Body.String())\n\t}\n}\n\nfunc TestTopologyCodexSubscription(t *testing.T) {\n\tcfg := subscriptionConfig(\"local-key\", \"\")\n\ttop := New(cfg, &store.Store{}, nil).topology()\n\tu := top.Upstreams[0]\n\tif u.AuthMode != string(config.AuthModeCodexSubscription) ||\n\t\tu.Auth != string(config.AuthModeCodexSubscription) || !u.Experimental {\n\t\tt.Fatalf(\"subscription upstream = %+v\", u)\n\t}\n\tif u.Endpoint != \"https://chatgpt.com/backend-api/codex/responses\" ||\n\t\tu.AuthHdr != \"Authorization: Bearer (client credential)\" {\n\t\tt.Fatalf(\"subscription endpoint/auth = %+v\", u)\n\t}\n\tcompact := top.Routes[1]\n\tif compact.Operation != string(config.RouteOperationResponsesCompact) ||\n\t\tcompact.Rules[0].Endpoint != \"https://chatgpt.com/backend-api/codex/responses/compact\" {\n\t\tt.Fatalf(\"compact route = %+v\", compact)\n\t}\n}\n"}}}} |