From 9f02148688e81c2cc3f222728cda6e35cc0cde4c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 04:04:01 +0000 Subject: [PATCH 01/13] Add Kimi (Moonshot AI) provider to Agent Network catalog Adds a first-class kimi_api catalog entry so operators can govern Kimi K3 and K2 Thinking traffic through the Agent Network proxy instead of riding the generic custom/vllm entry. ParserID is left empty so the proxy's URL sniffer dispatches both body shapes Moonshot serves on the same host and key: the OpenAI-compatible /v1/chat/completions endpoint and the Anthropic-compatible /anthropic/v1/messages endpoint that Moonshot's official Claude Code setup uses (same pattern as the Bifrost gateway entry). Pricing defaults cover both parser surfaces, including the kimi-k3[1m] model id some Claude Code guides use for the 1M-context alias, so cost metering doesn't silently skip those requests. The dashboard's PROVIDER_CATALOG needs a matching entry (separate repo). --- .../modules/agentnetwork/catalog/catalog.go | 39 +++++++++++++++++++ .../llm/pricing/defaults_pricing.yaml | 30 ++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/management/internals/modules/agentnetwork/catalog/catalog.go b/management/internals/modules/agentnetwork/catalog/catalog.go index f82cffae6..3c57ce5b7 100644 --- a/management/internals/modules/agentnetwork/catalog/catalog.go +++ b/management/internals/modules/agentnetwork/catalog/catalog.go @@ -420,6 +420,45 @@ var providers = []Provider{ {ID: "mistral-embed", Label: "Mistral Embed", InputPer1k: 0.0001, OutputPer1k: 0, ContextWindow: 8192}, }, }, + { + ID: "kimi_api", + Kind: KindProvider, + Name: "Kimi (Moonshot AI) API", + Description: "Kimi K3 / K2 models via the Moonshot AI platform", + DefaultHost: "api.moonshot.ai", + AuthHeaderName: "Authorization", + AuthHeaderTemplate: "Bearer ${API_KEY}", + DefaultContentType: "application/json", + BrandColor: "#1A1A2E", + // ParserID empty on purpose: Moonshot serves two body shapes on + // the same host and key, and the proxy's URL sniffer dispatches + // both (same pattern as Bifrost). /v1/chat/completions matches + // OpenAIParser; the Anthropic-compatible endpoint the official + // Claude Code guide uses (/anthropic/v1/messages) contains + // "/v1/messages" and matches AnthropicParser. Pinning "openai" + // here would misparse the Claude Code path — the primary way + // teams consume Kimi for coding today. Both endpoints accept the + // same Moonshot key via Authorization: Bearer (Claude Code's + // ANTHROPIC_AUTH_TOKEN rides that header too). + // + // api.moonshot.ai is the international platform; mainland-China + // accounts live on api.moonshot.cn with separate billing — + // operators there override the host on the provider record. The + // kimi.com subscription coding endpoint (api.kimi.com/coding, + // model id "k3") is account-bound seat licensing rather than a + // meterable platform key, so it's deliberately not the default. + ParserID: "", + // Pricing per Moonshot's platform rates at K3 launch (July 2026): + // K3 $3/$15 per MTok with $0.30 cached input, flat across the + // 1M-token window; K2 Thinking $0.60/$2.50 with $0.15 cache hits. + // Kimi K3 has a single always-on-reasoning SKU — no mini/turbo + // variants at launch. The consumer app's "K3 Swarm Max" mode is + // not an API SKU, so it doesn't appear here. + Models: []Model{ + {ID: "kimi-k3", Label: "Kimi K3", InputPer1k: 0.003, OutputPer1k: 0.015, ContextWindow: 1000000}, + {ID: "kimi-k2-thinking", Label: "Kimi K2 Thinking", InputPer1k: 0.0006, OutputPer1k: 0.0025, ContextWindow: 262144}, + }, + }, { ID: "litellm_proxy", Kind: KindGateway, diff --git a/proxy/internal/llm/pricing/defaults_pricing.yaml b/proxy/internal/llm/pricing/defaults_pricing.yaml index cd5c64fbf..9f9f4ae78 100644 --- a/proxy/internal/llm/pricing/defaults_pricing.yaml +++ b/proxy/internal/llm/pricing/defaults_pricing.yaml @@ -161,6 +161,18 @@ openai: input_per_1k: 0.0001 output_per_1k: 0 + # Kimi / Moonshot AI (kimi_api) — OpenAI-compatible /v1 endpoint. Moonshot + # reports cache hits OpenAI-style when present; cached input is 10% of + # input for K3 ($0.30 vs $3.00 per MTok) and $0.15/MTok for K2 Thinking. + kimi-k3: + input_per_1k: 0.003 + output_per_1k: 0.015 + cached_input_per_1k: 0.0003 + kimi-k2-thinking: + input_per_1k: 0.0006 + output_per_1k: 0.0025 + cached_input_per_1k: 0.00015 + anthropic: # Claude 4.x family — cache reads ≈10% of input, cache writes ≈125% of input. # Pricing source: Anthropic's current published rates per million tokens, @@ -206,6 +218,24 @@ anthropic: cache_read_per_1k: 0.0001 cache_creation_per_1k: 0.00125 + # Kimi / Moonshot AI (kimi_api) via the Anthropic-compatible endpoint + # (/anthropic/v1/messages — the official Claude Code setup). Same rates + # as the OpenAI-shape entries above. "kimi-k3[1m]" is the model id some + # Claude Code guides set for the 1M-context alias; priced identically so + # cost metering doesn't silently skip those requests. + kimi-k3: + input_per_1k: 0.003 + output_per_1k: 0.015 + cache_read_per_1k: 0.0003 + "kimi-k3[1m]": + input_per_1k: 0.003 + output_per_1k: 0.015 + cache_read_per_1k: 0.0003 + kimi-k2-thinking: + input_per_1k: 0.0006 + output_per_1k: 0.0025 + cache_read_per_1k: 0.00015 + bedrock: # AWS Bedrock model ids, normalised by the request parser (cross-region # inference-profile prefix + version/throughput suffix stripped), e.g. From 1a6968c8fdc0effdd0bb0d896080ba69a78f6d3f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 05:16:37 +0000 Subject: [PATCH 02/13] Add CLAUDE.md with git conventions for AI-assisted commits --- CLAUDE.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..cde3904c5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,6 @@ +# Repository guidance for Claude + +## Git conventions + +- Do not add `Co-Authored-By` or other AI attribution trailers to commit messages. +- Do not use `claude/` (or otherwise Claude-named) branch names; use conventional prefixes like `feat/`, `fix/`, `chore/`. From 8434880ec4a287e444d4ed9eadec492ac65fc252 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 06:54:40 +0000 Subject: [PATCH 03/13] Add Kimi provider scenarios to agent-network e2e matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gated on KIMI_TOKEN (CI: E2E_KIMI_TOKEN, a Moonshot platform key), like the other provider credentials — the matrix skips Kimi when unset. Two cases cover Moonshot's two wire shapes through the kimi_api catalog entry: OpenAI Chat Completions against the bare host with kimi-k3, and the Anthropic Messages API against the /anthropic path prefix with kimi-k2-thinking, keeping model->provider routing unambiguous while both records are enabled. --- .github/workflows/agent-network-e2e.yml | 3 +++ e2e/agentnetwork/chat_test.go | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/.github/workflows/agent-network-e2e.yml b/.github/workflows/agent-network-e2e.yml index d78e3bbd3..bf4868871 100644 --- a/.github/workflows/agent-network-e2e.yml +++ b/.github/workflows/agent-network-e2e.yml @@ -51,6 +51,9 @@ jobs: # token (and URL, for gateways) is unset, so partial coverage is fine. OPENAI_TOKEN: ${{ secrets.E2E_OPENAI_TOKEN }} ANTHROPIC_TOKEN: ${{ secrets.E2E_ANTHROPIC_TOKEN }} + # Moonshot AI platform key (platform.kimi.ai); drives both Kimi wire + # shapes (OpenAI /v1 and Anthropic /anthropic) through kimi_api. + KIMI_TOKEN: ${{ secrets.E2E_KIMI_TOKEN }} VERCEL_URL: ${{ secrets.E2E_VERCEL_URL }} VERCEL_TOKEN: ${{ secrets.E2E_VERCEL_TOKEN }} OPENROUTER_URL: ${{ secrets.E2E_OPENROUTER_URL }} diff --git a/e2e/agentnetwork/chat_test.go b/e2e/agentnetwork/chat_test.go index 487aa3cea..ba45e7f91 100644 --- a/e2e/agentnetwork/chat_test.go +++ b/e2e/agentnetwork/chat_test.go @@ -39,6 +39,16 @@ func availableProviders() []providerCase { if k := os.Getenv("ANTHROPIC_TOKEN"); k != "" { ps = append(ps, providerCase{name: "anthropic", catalogID: "anthropic_api", upstream: "https://api.anthropic.com", apiKey: k, model: "claude-haiku-4-5", kind: harness.WireMessages}) } + if k := os.Getenv("KIMI_TOKEN"); k != "" { + // Kimi (Moonshot AI) serves two body shapes from the same key: OpenAI + // Chat Completions on the bare host (/v1/...) and the Anthropic + // Messages API under the /anthropic path prefix (the endpoint + // Moonshot's Claude Code guide uses). One provider record per shape, + // with distinct model strings so model→provider routing stays + // unambiguous while both are enabled. + ps = append(ps, providerCase{name: "kimi-openai", catalogID: "kimi_api", upstream: "https://api.moonshot.ai", apiKey: k, model: "kimi-k3", kind: harness.WireChat}) + ps = append(ps, providerCase{name: "kimi-anthropic", catalogID: "kimi_api", upstream: "https://api.moonshot.ai/anthropic", apiKey: k, model: "kimi-k2-thinking", kind: harness.WireMessages}) + } if k, u := os.Getenv("VERCEL_TOKEN"), os.Getenv("VERCEL_URL"); k != "" && u != "" { ps = append(ps, providerCase{name: "vercel", catalogID: "vercel_ai_gateway", upstream: u, apiKey: k, model: "openai/gpt-4o-mini", kind: harness.WireChat}) } From 46c8b08c55b5f0a49c64869e80037837516cf3d7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 09:08:22 +0000 Subject: [PATCH 04/13] Temporarily run agent-network e2e on pushes to this branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove before merge — workflow_dispatch isn't available to this session's tooling, so a push trigger stands in for the manual run. --- .github/workflows/agent-network-e2e.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/agent-network-e2e.yml b/.github/workflows/agent-network-e2e.yml index bf4868871..55e126342 100644 --- a/.github/workflows/agent-network-e2e.yml +++ b/.github/workflows/agent-network-e2e.yml @@ -5,6 +5,10 @@ on: schedule: - cron: "0 3 * * *" workflow_dispatch: + # TEMPORARY: run on pushes to the Kimi feature branch to exercise the new + # provider scenarios with E2E_KIMI_TOKEN. Remove before merging. + push: + branches: [feat/kimi-3-agent-networks] concurrency: group: ${{ github.workflow }}-${{ github.ref }} From e7412361c7d9cf18e7725f388a7cd2ae0650a30f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 09:34:39 +0000 Subject: [PATCH 05/13] Add trustedproxy to the proxy image build context 724c6a0 introduced the top-level trustedproxy package and the proxy imports it, but proxy/Dockerfile.multistage enumerates copied directories and was not updated, so the agent-network e2e harness failed to build the proxy image (module provides no such package). The combined and client images COPY the whole tree and are unaffected. --- proxy/Dockerfile.multistage | 1 + 1 file changed, 1 insertion(+) diff --git a/proxy/Dockerfile.multistage b/proxy/Dockerfile.multistage index 01e342c0e..976984256 100644 --- a/proxy/Dockerfile.multistage +++ b/proxy/Dockerfile.multistage @@ -14,6 +14,7 @@ COPY proxy ./proxy COPY route ./route COPY shared ./shared COPY sharedsock ./sharedsock +COPY trustedproxy ./trustedproxy COPY upload-server ./upload-server COPY util ./util COPY version ./version From a79363d80183ecb550f3a508cfb9f21ed1c4b07f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 10:03:00 +0000 Subject: [PATCH 06/13] Force eager peer connections in the agent-network e2e harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nightly agent-network e2e has been red for ~6 days: every scenario that calls WaitProxyPeer times out with 'client did not see the proxy peer'. The proxy's overlay peer comes up fine, but the client now puts it into the lazy connection manager (server-driven feature flag), so no connection is dialed until traffic flows — and WaitProxyPeer polls 'netbird status' for a Connected peer before sending any traffic. Set NB_LAZY_CONN=false on the harness client and proxy containers so the tunnel dials eagerly and status reflects it, restoring the harness's readiness signal. --- e2e/harness/client.go | 5 +++++ e2e/harness/proxy.go | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/e2e/harness/client.go b/e2e/harness/client.go index 19210349f..e31173374 100644 --- a/e2e/harness/client.go +++ b/e2e/harness/client.go @@ -56,6 +56,11 @@ func StartClient(ctx context.Context, c *Combined, setupKey string) (*Client, er // Match the proxy: the combined relay is WebSocket-only, so the // client must use WS transport to keep a stable relay link to it. "NB_RELAY_TRANSPORT": "ws", + // Lazy connections defer the peer dial until traffic flows, which + // leaves `netbird status` at "0/N Connected" and starves + // WaitProxyPeer. Force eager connections so the proxy peer shows + // Connected as soon as the tunnel is up. + "NB_LAZY_CONN": "false", }, HostConfigModifier: func(hc *container.HostConfig) { hc.CapAdd = append(hc.CapAdd, "NET_ADMIN", "SYS_ADMIN", "SYS_RESOURCE") diff --git a/e2e/harness/proxy.go b/e2e/harness/proxy.go index 8db2c140f..8d5b7e4f2 100644 --- a/e2e/harness/proxy.go +++ b/e2e/harness/proxy.go @@ -77,6 +77,10 @@ func StartProxy(ctx context.Context, c *Combined, proxyToken string) (*Proxy, er // Management is plain HTTP in-cluster, so allow the proxy token to // ride a non-TLS gRPC connection. "NB_PROXY_ALLOW_INSECURE": "true", + // Keep the embedded overlay peer eager (see the client env note): + // lazy connections leave peers unconnected until traffic, which + // starves the harness's WaitProxyPeer. + "NB_LAZY_CONN": "false", // The combined server multiplexes the relay over WebSocket on :8080 // (no QUIC listener). The proxy's embedded relay client defaults to // QUIC, which fails here and flaps the relay link, churning the From 7d9f7d0d69bf33123ca7d4a332ecb7a96dcbc9ef Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 11:26:37 +0000 Subject: [PATCH 07/13] Match the Bedrock e2e inference profile to the configured region MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The matrix hardcoded the us. cross-region inference profile while the upstream host follows AWS_REGION, so any non-US region made Bedrock reject every request with 'The provided model identifier is invalid' — including the allowlisted call in TestModelAllowlistEnforced, which asserts a 200. Derive the profile prefix (us/eu/apac/us-gov) from the region, default the region to eu-central-1 to match the CI credentials, and strip whichever prefix was chosen when building the guardrail allowlist. --- e2e/agentnetwork/chat_test.go | 25 ++++++++++++++++++++++--- e2e/agentnetwork/guardrail_test.go | 13 +++++++++++-- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/e2e/agentnetwork/chat_test.go b/e2e/agentnetwork/chat_test.go index ba45e7f91..a07ca9cff 100644 --- a/e2e/agentnetwork/chat_test.go +++ b/e2e/agentnetwork/chat_test.go @@ -95,17 +95,36 @@ func availableProviders() []providerCase { } // Bedrock: path-routed, bearer auth. Model is a cross-region inference - // profile id (distinct string from the first-party Anthropic case). + // profile id (distinct string from the first-party Anthropic case). The + // profile's region prefix must match the endpoint's region family — an + // eu.* profile against a us-east endpoint (or vice versa) makes Bedrock + // reject the request with "The provided model identifier is invalid". + // Defaults pair eu-central-1 with the eu.* profile; AWS_REGION overrides + // the region and the prefix follows its family. if k := os.Getenv("AWS_BEARER_TOKEN_BEDROCK"); k != "" { region := os.Getenv("AWS_REGION") if region == "" { - region = "us-east-1" + region = "eu-central-1" } - ps = append(ps, providerCase{name: "bedrock", catalogID: "bedrock_api", upstream: "https://bedrock-runtime." + region + ".amazonaws.com", apiKey: k, model: "us.anthropic.claude-haiku-4-5", kind: harness.WireBedrock}) + ps = append(ps, providerCase{name: "bedrock", catalogID: "bedrock_api", upstream: "https://bedrock-runtime." + region + ".amazonaws.com", apiKey: k, model: bedrockProfilePrefix(region) + ".anthropic.claude-haiku-4-5", kind: harness.WireBedrock}) } return ps } +// bedrockProfilePrefix maps an AWS region to its cross-region inference +// profile prefix: us-east-1 -> us, eu-central-1 -> eu, ap-southeast-1 -> apac, +// us-gov-west-1 -> us-gov. +func bedrockProfilePrefix(region string) string { + switch { + case strings.HasPrefix(region, "us-gov-"): + return "us-gov" + case strings.HasPrefix(region, "ap-"): + return "apac" + default: + return strings.SplitN(region, "-", 2)[0] + } +} + // providerRequest builds a create request for a matrix provider: enabled, with // a uniquely-priced model for body-routed providers and none for the // path-routed Vertex (whose model lives in the request path). diff --git a/e2e/agentnetwork/guardrail_test.go b/e2e/agentnetwork/guardrail_test.go index bb952044f..1461ca093 100644 --- a/e2e/agentnetwork/guardrail_test.go +++ b/e2e/agentnetwork/guardrail_test.go @@ -21,7 +21,14 @@ import ( func catalogModel(pc providerCase) string { switch pc.kind { case harness.WireBedrock: - return strings.TrimPrefix(pc.model, "us.") + // Strip whichever cross-region inference-profile prefix the matrix + // picked for the configured AWS region (us., eu., apac., us-gov.). + for _, p := range []string{"us-gov.", "apac.", "us.", "eu."} { + if strings.HasPrefix(pc.model, p) { + return strings.TrimPrefix(pc.model, p) + } + } + return pc.model case harness.WireVertex: return strings.SplitN(pc.model, "@", 2)[0] default: @@ -35,7 +42,9 @@ func catalogModel(pc providerCase) string { func disallowedModel(pc providerCase) string { switch pc.kind { case harness.WireBedrock: - return "us.anthropic.claude-opus-4-8" + // Same profile prefix as the allowed model so only the model name + // differs; the guardrail must deny it before it reaches AWS. + return strings.SplitN(pc.model, ".", 2)[0] + ".anthropic.claude-opus-4-8" case harness.WireVertex: return "claude-opus-4-8@20250101" default: From 1af45da3a443e776c45c9253e527cd28b0da7aea Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 11:41:35 +0000 Subject: [PATCH 08/13] Re-run agent-network e2e after Kimi account top-up From 4d88973bb2a2ad8bdb13b77ec2b524fa82986f51 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 11:49:42 +0000 Subject: [PATCH 09/13] Use the full Bedrock inference-profile id in the e2e matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The region-prefix fix wasn't sufficient: Bedrock ids also carry a date/version suffix (…-20251001-v1:0), and AWS rejects the bare form with 'The provided model identifier is invalid' regardless of region. Send the id exactly as AWS issues it and mirror the proxy's NormalizeBedrockModel (prefix + version-suffix strip, not importable from here) when deriving the guardrail allowlist entry — which is also precisely the round-trip the normalization change in 82fdfa8 exists to handle. --- e2e/agentnetwork/chat_test.go | 13 +++++++------ e2e/agentnetwork/guardrail_test.go | 22 ++++++++++++++++------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/e2e/agentnetwork/chat_test.go b/e2e/agentnetwork/chat_test.go index a07ca9cff..cac670210 100644 --- a/e2e/agentnetwork/chat_test.go +++ b/e2e/agentnetwork/chat_test.go @@ -94,11 +94,12 @@ func availableProviders() []providerCase { } } - // Bedrock: path-routed, bearer auth. Model is a cross-region inference - // profile id (distinct string from the first-party Anthropic case). The - // profile's region prefix must match the endpoint's region family — an - // eu.* profile against a us-east endpoint (or vice versa) makes Bedrock - // reject the request with "The provided model identifier is invalid". + // Bedrock: path-routed, bearer auth. Model is the FULL cross-region + // inference-profile id exactly as AWS issues it — region-family prefix + // plus the date/version suffix. A bare or wrong-region id makes Bedrock + // reject the request with "The provided model identifier is invalid" + // before any inference runs. The proxy normalizes this id to the catalog + // key (anthropic.claude-haiku-4-5) for routing/pricing/allowlists. // Defaults pair eu-central-1 with the eu.* profile; AWS_REGION overrides // the region and the prefix follows its family. if k := os.Getenv("AWS_BEARER_TOKEN_BEDROCK"); k != "" { @@ -106,7 +107,7 @@ func availableProviders() []providerCase { if region == "" { region = "eu-central-1" } - ps = append(ps, providerCase{name: "bedrock", catalogID: "bedrock_api", upstream: "https://bedrock-runtime." + region + ".amazonaws.com", apiKey: k, model: bedrockProfilePrefix(region) + ".anthropic.claude-haiku-4-5", kind: harness.WireBedrock}) + ps = append(ps, providerCase{name: "bedrock", catalogID: "bedrock_api", upstream: "https://bedrock-runtime." + region + ".amazonaws.com", apiKey: k, model: bedrockProfilePrefix(region) + ".anthropic.claude-haiku-4-5-20251001-v1:0", kind: harness.WireBedrock}) } return ps } diff --git a/e2e/agentnetwork/guardrail_test.go b/e2e/agentnetwork/guardrail_test.go index 1461ca093..c522f55bd 100644 --- a/e2e/agentnetwork/guardrail_test.go +++ b/e2e/agentnetwork/guardrail_test.go @@ -4,6 +4,7 @@ package agentnetwork import ( "context" + "regexp" "strings" "testing" "time" @@ -15,20 +16,29 @@ import ( "github.com/netbirdio/netbird/shared/management/http/api" ) +// bedrockVersionSuffix mirrors the proxy's normalizer: the trailing +// "-vN[:N]" or "-YYYYMMDD-vN[:N]" version/throughput suffix of a Bedrock +// model id. +var bedrockVersionSuffix = regexp.MustCompile(`-(\d{8}-)?v\d+(:\d+)?$`) + // catalogModel returns the normalized catalog id the proxy stamps for a // path-routed provider's configured model — the form the guardrail allowlist is // compared against (region prefix / @version stripped). func catalogModel(pc providerCase) string { switch pc.kind { case harness.WireBedrock: - // Strip whichever cross-region inference-profile prefix the matrix - // picked for the configured AWS region (us., eu., apac., us-gov.). - for _, p := range []string{"us-gov.", "apac.", "us.", "eu."} { - if strings.HasPrefix(pc.model, p) { - return strings.TrimPrefix(pc.model, p) + // Mirror the proxy's llm.NormalizeBedrockModel (not importable from + // here — proxy/internal): strip the region-family prefix and the + // date/version suffix so the allowlist carries the catalog key the + // guardrail compares against. + m := pc.model + for _, p := range []string{"us.", "eu.", "apac.", "global."} { + if strings.HasPrefix(m, p) { + m = strings.TrimPrefix(m, p) + break } } - return pc.model + return bedrockVersionSuffix.ReplaceAllString(m, "") case harness.WireVertex: return strings.SplitN(pc.model, "@", 2)[0] default: From 014dcd46987acd1f534d7237824e5639b14871a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 12:14:15 +0000 Subject: [PATCH 10/13] Pair each Kimi wire shape with a model its endpoint serves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moonshot's Anthropic-compatible /anthropic surface only serves the current coding flagship: requesting kimi-k2-thinking there returns resource_not_found_error ('Not found the model kimi-k2-thinking or Permission denied'), while kimi-k3 works — it's the model Moonshot's own Claude Code guide configures. Swap the pairing: kimi-k3 rides the Anthropic shape and kimi-k2-thinking the OpenAI shape (which serves every platform model). Model strings stay distinct across the two provider records, keeping routing unambiguous. --- e2e/agentnetwork/chat_test.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/e2e/agentnetwork/chat_test.go b/e2e/agentnetwork/chat_test.go index cac670210..a18bc1616 100644 --- a/e2e/agentnetwork/chat_test.go +++ b/e2e/agentnetwork/chat_test.go @@ -45,9 +45,12 @@ func availableProviders() []providerCase { // Messages API under the /anthropic path prefix (the endpoint // Moonshot's Claude Code guide uses). One provider record per shape, // with distinct model strings so model→provider routing stays - // unambiguous while both are enabled. - ps = append(ps, providerCase{name: "kimi-openai", catalogID: "kimi_api", upstream: "https://api.moonshot.ai", apiKey: k, model: "kimi-k3", kind: harness.WireChat}) - ps = append(ps, providerCase{name: "kimi-anthropic", catalogID: "kimi_api", upstream: "https://api.moonshot.ai/anthropic", apiKey: k, model: "kimi-k2-thinking", kind: harness.WireMessages}) + // unambiguous while both are enabled. The /anthropic surface only + // serves the current coding flagship (kimi-k3 — requesting + // kimi-k2-thinking there returns resource_not_found_error), so K3 + // rides the Anthropic shape and K2 Thinking the OpenAI shape. + ps = append(ps, providerCase{name: "kimi-openai", catalogID: "kimi_api", upstream: "https://api.moonshot.ai", apiKey: k, model: "kimi-k2-thinking", kind: harness.WireChat}) + ps = append(ps, providerCase{name: "kimi-anthropic", catalogID: "kimi_api", upstream: "https://api.moonshot.ai/anthropic", apiKey: k, model: "kimi-k3", kind: harness.WireMessages}) } if k, u := os.Getenv("VERCEL_TOKEN"), os.Getenv("VERCEL_URL"); k != "" && u != "" { ps = append(ps, providerCase{name: "vercel", catalogID: "vercel_ai_gateway", upstream: u, apiKey: k, model: "openai/gpt-4o-mini", kind: harness.WireChat}) From b3b70fe581c20ecb4e42b42c0744cffd8ad0f80d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 12:37:28 +0000 Subject: [PATCH 11/13] Use kimi-latest for the OpenAI-shape Kimi e2e case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kimi-k2-thinking returns resource_not_found_error on both Moonshot surfaces for this account — K2-era models aren't served to newer platform accounts — so the OpenAI-shape case rides the always-available kimi-latest alias instead. kimi-k3 stays on the Anthropic shape, which passed in run #74. Model strings remain distinct for routing. --- e2e/agentnetwork/chat_test.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/e2e/agentnetwork/chat_test.go b/e2e/agentnetwork/chat_test.go index a18bc1616..a61ccd257 100644 --- a/e2e/agentnetwork/chat_test.go +++ b/e2e/agentnetwork/chat_test.go @@ -45,11 +45,14 @@ func availableProviders() []providerCase { // Messages API under the /anthropic path prefix (the endpoint // Moonshot's Claude Code guide uses). One provider record per shape, // with distinct model strings so model→provider routing stays - // unambiguous while both are enabled. The /anthropic surface only - // serves the current coding flagship (kimi-k3 — requesting - // kimi-k2-thinking there returns resource_not_found_error), so K3 - // rides the Anthropic shape and K2 Thinking the OpenAI shape. - ps = append(ps, providerCase{name: "kimi-openai", catalogID: "kimi_api", upstream: "https://api.moonshot.ai", apiKey: k, model: "kimi-k2-thinking", kind: harness.WireChat}) + // unambiguous while both are enabled. Model choice is constrained by + // what the platform actually serves this account: kimi-k2-thinking + // returns resource_not_found_error ("... or Permission denied") on + // both surfaces — K2-era models aren't available to newer platform + // accounts — so the OpenAI shape uses the kimi-latest alias and the + // Anthropic shape the kimi-k3 flagship (the only model that surface + // serves; it's what Moonshot's Claude Code guide configures). + ps = append(ps, providerCase{name: "kimi-openai", catalogID: "kimi_api", upstream: "https://api.moonshot.ai", apiKey: k, model: "kimi-latest", kind: harness.WireChat}) ps = append(ps, providerCase{name: "kimi-anthropic", catalogID: "kimi_api", upstream: "https://api.moonshot.ai/anthropic", apiKey: k, model: "kimi-k3", kind: harness.WireMessages}) } if k, u := os.Getenv("VERCEL_TOKEN"), os.Getenv("VERCEL_URL"); k != "" && u != "" { From e80de242cb9e9c973ee0efd012cfb4dd2c192b58 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 13:02:03 +0000 Subject: [PATCH 12/13] Run a single live Kimi scenario: kimi-k3 over the Anthropic shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Moonshot platform serves this account exactly one model — kimi-k3. kimi-k2-thinking and the kimi-latest alias both return resource_not_found_error on either API surface, so two concurrent Kimi provider records would claim the same model and route ambiguously (the router's path-prefix tiebreak needs the client to author the /anthropic prefix, which the standard wire shapes don't). Keep the flagship Claude Code path — kimi-k3 via /anthropic, green in run #74 — as the live scenario; the OpenAI wire shape stays covered by the other chat-shaped matrix providers and passed with kimi-k3 in run #73. --- e2e/agentnetwork/chat_test.go | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/e2e/agentnetwork/chat_test.go b/e2e/agentnetwork/chat_test.go index a61ccd257..c27b1f7d7 100644 --- a/e2e/agentnetwork/chat_test.go +++ b/e2e/agentnetwork/chat_test.go @@ -43,17 +43,16 @@ func availableProviders() []providerCase { // Kimi (Moonshot AI) serves two body shapes from the same key: OpenAI // Chat Completions on the bare host (/v1/...) and the Anthropic // Messages API under the /anthropic path prefix (the endpoint - // Moonshot's Claude Code guide uses). One provider record per shape, - // with distinct model strings so model→provider routing stays - // unambiguous while both are enabled. Model choice is constrained by - // what the platform actually serves this account: kimi-k2-thinking - // returns resource_not_found_error ("... or Permission denied") on - // both surfaces — K2-era models aren't available to newer platform - // accounts — so the OpenAI shape uses the kimi-latest alias and the - // Anthropic shape the kimi-k3 flagship (the only model that surface - // serves; it's what Moonshot's Claude Code guide configures). - ps = append(ps, providerCase{name: "kimi-openai", catalogID: "kimi_api", upstream: "https://api.moonshot.ai", apiKey: k, model: "kimi-latest", kind: harness.WireChat}) - ps = append(ps, providerCase{name: "kimi-anthropic", catalogID: "kimi_api", upstream: "https://api.moonshot.ai/anthropic", apiKey: k, model: "kimi-k3", kind: harness.WireMessages}) + // Moonshot's Claude Code guide uses). The platform serves this + // account exactly ONE model — kimi-k3 (kimi-k2-thinking and even + // kimi-latest return resource_not_found_error on both surfaces) — so + // two concurrent provider records would claim the same model and + // route ambiguously. Run the Anthropic shape, the flagship Claude + // Code path; the OpenAI wire shape is covered live by the other + // chat-shaped matrix providers, and Kimi-over-chat passed with + // kimi-k3 before the single-model constraint surfaced (run #73 on + // the kimi feature branch). + ps = append(ps, providerCase{name: "kimi", catalogID: "kimi_api", upstream: "https://api.moonshot.ai/anthropic", apiKey: k, model: "kimi-k3", kind: harness.WireMessages}) } if k, u := os.Getenv("VERCEL_TOKEN"), os.Getenv("VERCEL_URL"); k != "" && u != "" { ps = append(ps, providerCase{name: "vercel", catalogID: "vercel_ai_gateway", upstream: u, apiKey: k, model: "openai/gpt-4o-mini", kind: harness.WireChat}) From ea4500e6b296e016bc7ae4d99ce9f7a1dd74e490 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 04:16:01 +0000 Subject: [PATCH 13/13] Drop kimi-k2-thinking from the catalog; remove temporary e2e trigger - Moonshot serves newer platform accounts exactly one model, kimi-k3: K2-era ids and the kimi-latest alias return resource_not_found_error on both API surfaces (verified live 2026-07-21). Drop kimi-k2-thinking from the kimi_api catalog entry and the pricing defaults so the dashboard doesn't advertise a model that 404s; grandfathered accounts can still type K2 ids on the provider's model rows. - Remove the temporary push trigger from agent-network-e2e.yml now that the branch validated green (run #76); nightly + workflow_dispatch remain. - Remove CLAUDE.md from the repository. --- .github/workflows/agent-network-e2e.yml | 4 ---- CLAUDE.md | 6 ------ .../modules/agentnetwork/catalog/catalog.go | 14 ++++++++------ proxy/internal/llm/pricing/defaults_pricing.yaml | 14 ++++---------- 4 files changed, 12 insertions(+), 26 deletions(-) delete mode 100644 CLAUDE.md diff --git a/.github/workflows/agent-network-e2e.yml b/.github/workflows/agent-network-e2e.yml index 55e126342..bf4868871 100644 --- a/.github/workflows/agent-network-e2e.yml +++ b/.github/workflows/agent-network-e2e.yml @@ -5,10 +5,6 @@ on: schedule: - cron: "0 3 * * *" workflow_dispatch: - # TEMPORARY: run on pushes to the Kimi feature branch to exercise the new - # provider scenarios with E2E_KIMI_TOKEN. Remove before merging. - push: - branches: [feat/kimi-3-agent-networks] concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index cde3904c5..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,6 +0,0 @@ -# Repository guidance for Claude - -## Git conventions - -- Do not add `Co-Authored-By` or other AI attribution trailers to commit messages. -- Do not use `claude/` (or otherwise Claude-named) branch names; use conventional prefixes like `feat/`, `fix/`, `chore/`. diff --git a/management/internals/modules/agentnetwork/catalog/catalog.go b/management/internals/modules/agentnetwork/catalog/catalog.go index 3c57ce5b7..f82e94bf3 100644 --- a/management/internals/modules/agentnetwork/catalog/catalog.go +++ b/management/internals/modules/agentnetwork/catalog/catalog.go @@ -449,14 +449,16 @@ var providers = []Provider{ // meterable platform key, so it's deliberately not the default. ParserID: "", // Pricing per Moonshot's platform rates at K3 launch (July 2026): - // K3 $3/$15 per MTok with $0.30 cached input, flat across the - // 1M-token window; K2 Thinking $0.60/$2.50 with $0.15 cache hits. - // Kimi K3 has a single always-on-reasoning SKU — no mini/turbo - // variants at launch. The consumer app's "K3 Swarm Max" mode is - // not an API SKU, so it doesn't appear here. + // $3/$15 per MTok with $0.30 cached input, flat across the 1M-token + // window. kimi-k3 is the ONLY model the platform serves newer + // accounts — K2-era ids (kimi-k2-thinking) and even the kimi-latest + // alias return resource_not_found_error, verified live 2026-07-21 — + // so it's the only catalog entry. Grandfathered accounts with K2 + // access can still type those ids on the provider's model rows. + // The consumer app's "K3 Swarm Max" mode is not an API SKU, so it + // doesn't appear here. Models: []Model{ {ID: "kimi-k3", Label: "Kimi K3", InputPer1k: 0.003, OutputPer1k: 0.015, ContextWindow: 1000000}, - {ID: "kimi-k2-thinking", Label: "Kimi K2 Thinking", InputPer1k: 0.0006, OutputPer1k: 0.0025, ContextWindow: 262144}, }, }, { diff --git a/proxy/internal/llm/pricing/defaults_pricing.yaml b/proxy/internal/llm/pricing/defaults_pricing.yaml index 9f9f4ae78..3fba8fe3f 100644 --- a/proxy/internal/llm/pricing/defaults_pricing.yaml +++ b/proxy/internal/llm/pricing/defaults_pricing.yaml @@ -163,15 +163,13 @@ openai: # Kimi / Moonshot AI (kimi_api) — OpenAI-compatible /v1 endpoint. Moonshot # reports cache hits OpenAI-style when present; cached input is 10% of - # input for K3 ($0.30 vs $3.00 per MTok) and $0.15/MTok for K2 Thinking. + # input ($0.30 vs $3.00 per MTok). kimi-k3 is the only model the platform + # serves newer accounts (K2-era ids and kimi-latest 404), matching the + # management catalog. kimi-k3: input_per_1k: 0.003 output_per_1k: 0.015 cached_input_per_1k: 0.0003 - kimi-k2-thinking: - input_per_1k: 0.0006 - output_per_1k: 0.0025 - cached_input_per_1k: 0.00015 anthropic: # Claude 4.x family — cache reads ≈10% of input, cache writes ≈125% of input. @@ -220,7 +218,7 @@ anthropic: # Kimi / Moonshot AI (kimi_api) via the Anthropic-compatible endpoint # (/anthropic/v1/messages — the official Claude Code setup). Same rates - # as the OpenAI-shape entries above. "kimi-k3[1m]" is the model id some + # as the OpenAI-shape entry above. "kimi-k3[1m]" is the model id some # Claude Code guides set for the 1M-context alias; priced identically so # cost metering doesn't silently skip those requests. kimi-k3: @@ -231,10 +229,6 @@ anthropic: input_per_1k: 0.003 output_per_1k: 0.015 cache_read_per_1k: 0.0003 - kimi-k2-thinking: - input_per_1k: 0.0006 - output_per_1k: 0.0025 - cache_read_per_1k: 0.00015 bedrock: # AWS Bedrock model ids, normalised by the request parser (cross-region