mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-27 23:09:08 +01:00
[management, proxy] add Kimi (Moonshot AI) provider to Agent Network (#6853)
This commit is contained in:
@@ -51,6 +51,9 @@ jobs:
|
|||||||
# token (and URL, for gateways) is unset, so partial coverage is fine.
|
# token (and URL, for gateways) is unset, so partial coverage is fine.
|
||||||
OPENAI_TOKEN: ${{ secrets.E2E_OPENAI_TOKEN }}
|
OPENAI_TOKEN: ${{ secrets.E2E_OPENAI_TOKEN }}
|
||||||
ANTHROPIC_TOKEN: ${{ secrets.E2E_ANTHROPIC_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_URL: ${{ secrets.E2E_VERCEL_URL }}
|
||||||
VERCEL_TOKEN: ${{ secrets.E2E_VERCEL_TOKEN }}
|
VERCEL_TOKEN: ${{ secrets.E2E_VERCEL_TOKEN }}
|
||||||
OPENROUTER_URL: ${{ secrets.E2E_OPENROUTER_URL }}
|
OPENROUTER_URL: ${{ secrets.E2E_OPENROUTER_URL }}
|
||||||
|
|||||||
@@ -20,14 +20,15 @@ import (
|
|||||||
// covers whatever credentials are present (source ~/.llm-keys locally / set the
|
// covers whatever credentials are present (source ~/.llm-keys locally / set the
|
||||||
// Actions secrets in CI).
|
// Actions secrets in CI).
|
||||||
type providerCase struct {
|
type providerCase struct {
|
||||||
name string
|
name string
|
||||||
catalogID string
|
catalogID string
|
||||||
upstream string
|
upstream string
|
||||||
apiKey string
|
apiKey string
|
||||||
model string // body model (chat/messages) or path model@version (vertex)
|
model string // body model (chat/messages) or path model@version (vertex)
|
||||||
kind string // harness.WireChat, harness.WireMessages, or harness.WireVertex
|
kind string // harness.WireChat, harness.WireMessages, or harness.WireVertex
|
||||||
project string // vertex only: GCP project for the rawPredict path
|
project string // vertex only: GCP project for the rawPredict path
|
||||||
region string // vertex only: GCP region for the rawPredict path
|
region string // vertex only: GCP region for the rawPredict path
|
||||||
|
pathPrefix string // base-URL path prefix the agent carries (e.g. "/anthropic" for Kimi)
|
||||||
}
|
}
|
||||||
|
|
||||||
// availableProviders builds the matrix from the provider env vars that are set.
|
// availableProviders builds the matrix from the provider env vars that are set.
|
||||||
@@ -39,6 +40,24 @@ func availableProviders() []providerCase {
|
|||||||
if k := os.Getenv("ANTHROPIC_TOKEN"); k != "" {
|
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})
|
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). The provider keeps the bare
|
||||||
|
// default upstream and the AGENT carries the /anthropic prefix in
|
||||||
|
// its base URL — exactly the documented Claude Code / Kimi CLI
|
||||||
|
// setup (ANTHROPIC_BASE_URL=https://<endpoint>/anthropic) — so one
|
||||||
|
// provider serves both shapes and the prefix rides through to
|
||||||
|
// Moonshot. 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). 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).
|
||||||
|
ps = append(ps, providerCase{name: "kimi", catalogID: "kimi_api", upstream: "https://api.moonshot.ai", apiKey: k, model: "kimi-k3", kind: harness.WireMessages, pathPrefix: "/anthropic"})
|
||||||
|
}
|
||||||
if k, u := os.Getenv("VERCEL_TOKEN"), os.Getenv("VERCEL_URL"); k != "" && u != "" {
|
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})
|
ps = append(ps, providerCase{name: "vercel", catalogID: "vercel_ai_gateway", upstream: u, apiKey: k, model: "openai/gpt-4o-mini", kind: harness.WireChat})
|
||||||
}
|
}
|
||||||
@@ -84,12 +103,18 @@ func availableProviders() []providerCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bedrock: path-routed, bearer auth. Model is a cross-region inference
|
// Bedrock: path-routed, bearer auth. Model is the FULL cross-region
|
||||||
// profile id (distinct string from the first-party Anthropic case).
|
// 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 != "" {
|
if k := os.Getenv("AWS_BEARER_TOKEN_BEDROCK"); k != "" {
|
||||||
region := os.Getenv("AWS_REGION")
|
region := os.Getenv("AWS_REGION")
|
||||||
if region == "" {
|
if region == "" {
|
||||||
region = "us-east-1"
|
region = "eu-central-1"
|
||||||
}
|
}
|
||||||
// A valid Bedrock inference-profile id (region prefix + date + version),
|
// A valid Bedrock inference-profile id (region prefix + date + version),
|
||||||
// overridable per account. `global.` profiles can be invoked from any
|
// overridable per account. `global.` profiles can be invoked from any
|
||||||
@@ -246,7 +271,7 @@ func TestProvidersMatrix(t *testing.T) {
|
|||||||
case harness.WireBedrock:
|
case harness.WireBedrock:
|
||||||
c, b, cerr = cl.Bedrock(ctx, settings.Endpoint, proxyIP, pc.model, "Reply with exactly: pong", sessionID)
|
c, b, cerr = cl.Bedrock(ctx, settings.Endpoint, proxyIP, pc.model, "Reply with exactly: pong", sessionID)
|
||||||
default:
|
default:
|
||||||
c, b, cerr = cl.Chat(ctx, settings.Endpoint, proxyIP, pc.kind, pc.model, "Reply with exactly: pong", sessionID)
|
c, b, cerr = cl.ChatPrefixed(ctx, settings.Endpoint, proxyIP, pc.pathPrefix, pc.kind, pc.model, "Reply with exactly: pong", sessionID)
|
||||||
}
|
}
|
||||||
if cerr == nil {
|
if cerr == nil {
|
||||||
code, body = c, b
|
code, body = c, b
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ func catalogModel(pc providerCase) string {
|
|||||||
func disallowedModel(pc providerCase) string {
|
func disallowedModel(pc providerCase) string {
|
||||||
switch pc.kind {
|
switch pc.kind {
|
||||||
case harness.WireBedrock:
|
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:
|
case harness.WireVertex:
|
||||||
return "claude-opus-4-8@20250101"
|
return "claude-opus-4-8@20250101"
|
||||||
default:
|
default:
|
||||||
@@ -72,7 +74,7 @@ func sendModel(ctx context.Context, t *testing.T, cl *harness.Client, endpoint,
|
|||||||
case harness.WireVertex:
|
case harness.WireVertex:
|
||||||
code, _, err = cl.Vertex(ctx, endpoint, proxyIP, pc.project, pc.region, model, "Reply with exactly: pong", "")
|
code, _, err = cl.Vertex(ctx, endpoint, proxyIP, pc.project, pc.region, model, "Reply with exactly: pong", "")
|
||||||
default:
|
default:
|
||||||
code, _, err = cl.Chat(ctx, endpoint, proxyIP, pc.kind, model, "Reply with exactly: pong", "")
|
code, _, err = cl.ChatPrefixed(ctx, endpoint, proxyIP, pc.pathPrefix, pc.kind, model, "Reply with exactly: pong", "")
|
||||||
}
|
}
|
||||||
require.NoError(t, err, "request must reach the proxy for %s", pc.name)
|
require.NoError(t, err, "request must reach the proxy for %s", pc.name)
|
||||||
return code
|
return code
|
||||||
|
|||||||
+12
-1
@@ -239,6 +239,17 @@ const (
|
|||||||
// the wire shape: WireChat (OpenAI) or WireMessages (Anthropic). A non-empty
|
// the wire shape: WireChat (OpenAI) or WireMessages (Anthropic). A non-empty
|
||||||
// sessionID is sent as the universal x-session-id header the proxy records.
|
// sessionID is sent as the universal x-session-id header the proxy records.
|
||||||
func (cl *Client) Chat(ctx context.Context, endpoint, proxyIP, kind, model, prompt, sessionID string) (int, string, error) {
|
func (cl *Client) Chat(ctx context.Context, endpoint, proxyIP, kind, model, prompt, sessionID string) (int, string, error) {
|
||||||
|
return cl.ChatPrefixed(ctx, endpoint, proxyIP, "", kind, model, prompt, sessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatPrefixed is Chat with a base-URL path prefix prepended to the wire
|
||||||
|
// path, mirroring agents whose base URL carries a shape-selecting prefix that
|
||||||
|
// rides through to the upstream — e.g. Claude Code against a Kimi provider
|
||||||
|
// sets ANTHROPIC_BASE_URL=https://<endpoint>/anthropic so the proxy forwards
|
||||||
|
// /anthropic/v1/messages to Moonshot's Anthropic surface while the provider's
|
||||||
|
// upstream URL stays the bare https://api.moonshot.ai. Empty prefix is plain
|
||||||
|
// Chat.
|
||||||
|
func (cl *Client) ChatPrefixed(ctx context.Context, endpoint, proxyIP, pathPrefix, kind, model, prompt, sessionID string) (int, string, error) {
|
||||||
var path, body string
|
var path, body string
|
||||||
var headers []string
|
var headers []string
|
||||||
switch kind {
|
switch kind {
|
||||||
@@ -250,7 +261,7 @@ func (cl *Client) Chat(ctx context.Context, endpoint, proxyIP, kind, model, prom
|
|||||||
path = "/v1/chat/completions"
|
path = "/v1/chat/completions"
|
||||||
body = fmt.Sprintf(`{"model":%q,"messages":[{"role":"user","content":%q}]}`, model, prompt)
|
body = fmt.Sprintf(`{"model":%q,"messages":[{"role":"user","content":%q}]}`, model, prompt)
|
||||||
}
|
}
|
||||||
return cl.post(ctx, endpoint, proxyIP, path, body, withSessionID(headers, sessionID))
|
return cl.post(ctx, endpoint, proxyIP, pathPrefix+path, body, withSessionID(headers, sessionID))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vertex issues an Anthropic-on-Vertex rawPredict POST over the tunnel. Unlike
|
// Vertex issues an Anthropic-on-Vertex rawPredict POST over the tunnel. Unlike
|
||||||
|
|||||||
@@ -420,6 +420,47 @@ var providers = []Provider{
|
|||||||
{ID: "mistral-embed", Label: "Mistral Embed", InputPer1k: 0.0001, OutputPer1k: 0, ContextWindow: 8192},
|
{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):
|
||||||
|
// $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: "litellm_proxy",
|
ID: "litellm_proxy",
|
||||||
Kind: KindGateway,
|
Kind: KindGateway,
|
||||||
|
|||||||
@@ -161,6 +161,16 @@ openai:
|
|||||||
input_per_1k: 0.0001
|
input_per_1k: 0.0001
|
||||||
output_per_1k: 0
|
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 ($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
|
||||||
|
|
||||||
anthropic:
|
anthropic:
|
||||||
# Claude 4.x family — cache reads ≈10% of input, cache writes ≈125% of input.
|
# Claude 4.x family — cache reads ≈10% of input, cache writes ≈125% of input.
|
||||||
# Pricing source: Anthropic's current published rates per million tokens,
|
# Pricing source: Anthropic's current published rates per million tokens,
|
||||||
@@ -206,6 +216,20 @@ anthropic:
|
|||||||
cache_read_per_1k: 0.0001
|
cache_read_per_1k: 0.0001
|
||||||
cache_creation_per_1k: 0.00125
|
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 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:
|
||||||
|
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
|
||||||
|
|
||||||
bedrock:
|
bedrock:
|
||||||
# AWS Bedrock model ids, normalised by the request parser (cross-region
|
# AWS Bedrock model ids, normalised by the request parser (cross-region
|
||||||
# inference-profile prefix + version/throughput suffix stripped), e.g.
|
# inference-profile prefix + version/throughput suffix stripped), e.g.
|
||||||
|
|||||||
Reference in New Issue
Block a user