## Summary
- During streaming, `AbstractAgent.addMessage()` can push messages with
the same `id` into the messages array, causing React duplicate key
warnings in `CopilotChatMessageView`
- Added Map-based deduplication in the message rendering pipeline —
last-write-wins for duplicate IDs
- Includes dev-mode `console.warn` when duplicates are detected, to
surface the upstream cause without breaking the UI
Fixes#3258
## Test plan
- [x] Unit tests added verifying deduplication behavior
- [x] Warning logged in development when duplicates detected
- [x] Full package test suite passes
- [x] Build passes
🤖 Generated with [Claude Code](https://claude.ai/code)
Reverts the resolveAgent extraction from the previous commit — the
original useMemo + ref cache already provides referential stability.
All 4 stability tests pass without the refactor.
runtime-client-gql's graphql-codegen needs the schema file generated
by @copilotkit/runtime's build. After removing the package.json
dependency (to fix bundle size), Nx no longer orders the builds
correctly. implicitDependencies restores build ordering without
reintroducing the runtime dependency.
The @copilotkit/runtime-client-gql package listed @copilotkit/runtime as
a dependency, but none of its source files actually import from it. This
caused the entire server-side runtime (graphql-yoga, hono, openai, shiki,
reflect-metadata, etc.) to be pulled into client bundles, exploding
bundle size from ~600kb to 4.3mb+.
Remove the unused dependency from package.json, the UMD externals list,
and the UMD globals map in tsdown.config.ts.
Move V1 test file to __tests__/ directory and add test verifying that
string enum constraints round-trip through convertJsonSchemaToZodSchema.
Add equivalent test for V2's copy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The string case in convertJsonSchemaToZodSchema produced z.string() and
silently discarded the enum field. When useCopilotAction defines a
parameter with enum: ["todo", "done"], the constraint was lost during
the V1→V2 bridge and never reached the runtime payload.
Add an enum check before falling through to z.string() in both V1
(shared/json-schema.ts) and V2 (agent/index.ts) copies of the function.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The afterRequestMiddleware receives a cloned Response (via c.res.clone()),
whose internal stream controller functions have different names across
Node versions. Using expect.any(Response) avoids brittle deep equality
on Response internals.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Clone Response before body is consumed, parse SSE stream to reconstruct
messages. Adds messages, threadId, runId to AfterRequestMiddlewareParameters.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Next.js rejects global CSS imports from within node_modules, causing
build failures when consumers use @copilotkitnext/react:
Global CSS cannot be imported from within node_modules.
Location: node_modules/@copilotkitnext/react/dist/index.mjs
The static `import 'katex/dist/katex.min.css'` in
CopilotChatAssistantMessage.tsx gets preserved in the dist bundle
(tsdown unbundle mode), triggering the Next.js css-npm restriction.
Replace with a useKatexStyles() hook that dynamically imports the CSS
at runtime via useEffect. Dynamic imports bypass Next.js static
analysis and load the stylesheet lazily when the component mounts.
Fixes#2851
The shouldCreateModalState guard introduced in #3101 made setModalOpen
and isModalOpen conditionally undefined when isModalDefaultOpen wasn't
explicitly passed. This broke programmatic modal control for anyone
using useCopilotChatConfiguration outside of CopilotPopup/CopilotSidebar.
Reverts to the simpler ?? fallback pattern: each provider always has
modal state, preferring parent state when available, falling back to
its own internal state otherwise. Initial state still respects the
local isModalDefaultOpen prop (fix from 76e381f78).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The V1 CopilotKit provider hardcoded useSingleEndpoint={true}, preventing
users from opting into REST transport mode. Now passes through the user's
prop value, defaulting to true for backward compatibility.
Fixes#2903
## The Bug
Azure OpenAI users get `AI_LoadAPIKeyError: OpenAI API key is missing`
despite passing a properly configured `OpenAI` instance to
`OpenAIAdapter`, exactly as shown in the docs.
**Root cause**: `CopilotRuntime.handleServiceAdapter()` discards the
adapter's configured OpenAI instance and constructs a plain string
`"openai/gpt-4o"` to pass to `BuiltInAgent`. The agent's
`resolveModel()` then creates a *brand new* OpenAI provider using only
`OPENAI_API_KEY`, losing all Azure configuration (baseURL, api-key
header, api-version query param).
The V1 adapter's `process()` method — which correctly uses the custom
OpenAI instance — is dead code. It's never called. Only `.provider` and
`.model` strings are extracted.
```
User configures Azure OpenAI instance →
OpenAIAdapter stores it →
CopilotRuntime extracts only "openai"/"gpt-4o" strings →
BuiltInAgent.resolveModel("openai/gpt-4o") →
createOpenAI({ apiKey: OPENAI_API_KEY }) →
💥 API key missing / wrong endpoint
```
## Workaround
Until this is merged, users can bypass the broken adapter entirely by
using `BuiltInAgent` directly with the Vercel AI SDK Azure provider:
```typescript
import { createAzure } from "@ai-sdk/azure";
import { CopilotRuntime, copilotRuntimeNodeHttpEndpoint } from "@copilotkit/runtime";
import { BuiltInAgent } from "@copilotkitnext/agent";
const azure = createAzure({
resourceName: process.env.AZURE_RESOURCE_NAME,
apiKey: process.env.AZURE_API_KEY,
});
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({
model: azure("your-deployment-name"),
}),
},
});
// Then use copilotRuntimeNodeHttpEndpoint as usual — no serviceAdapter needed
```
This works because `BuiltInAgent` already accepts `LanguageModel`
instances and `resolveModel()` passes them straight through.
## The Fix
Add an optional `getLanguageModel()` method to `CopilotServiceAdapter`.
Adapters that accept custom SDK instances (OpenAI, Anthropic, Groq) now
implement this to produce a pre-configured Vercel AI SDK `LanguageModel`
that carries the full provider configuration. `CopilotRuntime` prefers
this over the string format when creating `BuiltInAgent`.
**Backward compatible**: `getLanguageModel()` is optional on the
interface, so existing custom adapters continue to work unchanged.
| File | Change |
|------|--------|
| `service-adapter.ts` | Add optional `getLanguageModel()` to
`CopilotServiceAdapter` interface |
| `openai-adapter.ts` | Implement `getLanguageModel()` using
`createOpenAI` with the configured instance's `baseURL`/`apiKey` |
| `anthropic-adapter.ts` | Implement `getLanguageModel()` using
`createAnthropic` |
| `groq-adapter.ts` | Implement `getLanguageModel()` using
`createOpenAI` (Groq is OpenAI-compatible) |
| `copilot-runtime.ts` | Prefer `serviceAdapter.getLanguageModel()` over
string model when creating `BuiltInAgent` |
| `package.json` | Add `ai`, `@ai-sdk/openai`, `@ai-sdk/anthropic`
dependencies |
## Test plan
- [x] V2 agent tests pass (101/101)
- [x] V1 runtime build succeeds with no type errors
- [x] V1 runtime tests: same pass/fail as `main` (pre-existing failures
unrelated to this change)
- [ ] Manual: configure Azure OpenAI via `OpenAIAdapter` without
`OPENAI_API_KEY` — should work without `AI_LoadAPIKeyError`
- [ ] Manual: standard OpenAI via `OpenAIAdapter` with `OPENAI_API_KEY`
— should continue working
🤖 Generated with [Claude Code](https://claude.com/claude-code)