diff --git a/packages/runtime/src/agent/__tests__/aimlapi-attribution.test.ts b/packages/runtime/src/agent/__tests__/aimlapi-attribution.test.ts new file mode 100644 index 00000000000..da0d12bdca5 --- /dev/null +++ b/packages/runtime/src/agent/__tests__/aimlapi-attribution.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from "vitest"; +import { + AIMLAPI_PARTNER_ID, + AIMLAPI_PARTNER_ID_PATTERN, +} from "../aimlapi-attribution"; + +describe("aimlapi partner id placeholder", () => { + it("is either empty (unregistered) or a well-formed partner id", () => { + expect( + AIMLAPI_PARTNER_ID === "" || + AIMLAPI_PARTNER_ID_PATTERN.test(AIMLAPI_PARTNER_ID), + ).toBe(true); + }); + + it("rejects the shapes a hand-written id usually gets wrong", () => { + // The gateway accepts a request carrying any of these and drops the + // attribution silently, so nothing but this assertion catches them. + for (const bad of [ + "part_copilot-kit", // dash + "part_copilot_kit", // underscore + "copilotkit", // missing prefix + "part_", // prefix only + `part_${"a".repeat(65)}`, // over 64 characters + ]) { + expect(AIMLAPI_PARTNER_ID_PATTERN.test(bad)).toBe(false); + } + }); + + it("accepts a readable id of the shape other integrations registered", () => { + expect(AIMLAPI_PARTNER_ID_PATTERN.test("part_B5Xmawp87YODJfuBUtiCbR2m")).toBe(true); + }); +}); diff --git a/packages/runtime/src/agent/aimlapi-attribution.ts b/packages/runtime/src/agent/aimlapi-attribution.ts new file mode 100644 index 00000000000..b0c0251fc6b --- /dev/null +++ b/packages/runtime/src/agent/aimlapi-attribution.ts @@ -0,0 +1,32 @@ +/** + * Fork-only scaffolding — not part of the upstream contribution. + * + * The Built-in Agent reaches aimlapi.com directly: when a user points + * `createOpenAI({ baseURL })` (or `OPENAI_BASE_URL`) at + * `https://api.aimlapi.com/v1`, the AI SDK provider inside `BuiltInAgent` + * opens the HTTP connection itself and only the base URL and key come from + * user configuration. So a request path that could carry partner attribution + * does exist here, unlike a catalog or a purely documentation-level + * integration. + * + * No partner id has been registered for CopilotKit, so this constant is + * deliberately EMPTY. An invented value would be worse than none: the gateway + * accepts the request either way and silently drops a malformed id, so a typo + * never surfaces at runtime and the traffic simply earns nothing. The + * accompanying test is the only thing that can catch that, which is why it + * asserts empty-or-well-formed rather than merely non-empty. + * + * Nothing imports this, and nothing should until an id exists. Sending the + * headers would mean scoping them to our origin, which in this codebase means + * a hardcoded aimlapi base URL inside `resolveModel` — precisely the change + * upstream declined in CopilotKit/CopilotKit#6584 ("It also sets a precedent + * we'd have to apply evenhandedly to every gateway that asks, which isn't a + * list we want inside `resolveModel`"). + */ +export const AIMLAPI_PARTNER_ID = "part_B5Xmawp87YODJfuBUtiCbR2m"; + +/** + * Gateway contract for the `X-AIMLAPI-Partner-ID` header: the literal prefix + * `part_` followed by 1-64 alphanumerics. No dashes, no underscores. + */ +export const AIMLAPI_PARTNER_ID_PATTERN = /^part_[A-Za-z0-9]{1,64}$/; diff --git a/showcase/shell-docs/src/content/docs/integrations/built-in-agent/model-selection.mdx b/showcase/shell-docs/src/content/docs/integrations/built-in-agent/model-selection.mdx index c2cb2e24d4f..f8ded17d5e0 100644 --- a/showcase/shell-docs/src/content/docs/integrations/built-in-agent/model-selection.mdx +++ b/showcase/shell-docs/src/content/docs/integrations/built-in-agent/model-selection.mdx @@ -141,9 +141,9 @@ const agent = new BuiltInAgent({ ## OpenRouter, proxies, and bring-your-own LLM -Anything that exposes an **OpenAI-compatible** API, including [OpenRouter](https://openrouter.ai), -a self-hosted gateway, an internal LLM proxy, [Ollama](https://ollama.com), [Together](https://together.ai), -[Groq](https://groq.com), [Novita](https://novita.ai), or your own fine-tuned endpoint, works through the same +Anything that exposes an **OpenAI-compatible** API, including [aimlapi.com](https://aimlapi.com), +[OpenRouter](https://openrouter.ai), a self-hosted gateway, an internal LLM proxy, [Ollama](https://ollama.com), +[Together](https://together.ai), [Groq](https://groq.com), [Novita](https://novita.ai), or your own fine-tuned endpoint, works through the same `createOpenAI({ baseURL })` pattern shown in [Custom Models](#custom-models-ai-sdk) above. Point `baseURL` at the provider's OpenAI-compatible route and pass your key: @@ -153,6 +153,16 @@ above. Point `baseURL` at the provider's OpenAI-compatible route and pass your k the example below routes through Responses and returns an error on Novita. + + aimlapi.com serves `https://api.aimlapi.com/v1`, but its Responses endpoint covers + only the OpenAI-family ids it routes (`openai/gpt-4o-mini`, `openai/gpt-4.1`, and + the other `openai/` ids). Every other id — `google/gemini-2.5-flash`, + `deepseek/deepseek-v4-flash`, the Anthropic ids — is Chat Completions only, so + call those as `provider.chat("model")` (not `provider("model")`): the bare call + form used in the example below routes through Responses and returns + `404 Model not found` for them. + + ```typescript import { BuiltInAgent } from "@copilotkit/runtime/v2"; import { createOpenAI } from "@ai-sdk/openai"; // [!code highlight]