Summary
handleGeminiInteractions in src/gemini-interactions.ts doesn't recognize the Step[] envelope shape that the live Gemini Interactions API requires at the top level of input. Clients sending the format Google's /v1beta/interactions actually accepts get an empty userMessage and 404 "No fixture matched".
What aimock currently handles
geminiInteractionsToCompletionRequest (dist/gemini-interactions.cjs:9) covers three input shapes:
if (typeof req.input === "string") { … } // ✓
else if (Array.isArray(req.input)) {
const firstItem = req.input[0];
if (firstItem && "role" in firstItem) { … Turn[] … } // ✓ (merged via #139)
else { // ✗ falls here for Step[]
const text = req.input
.filter(p => p.type === "text")
.map(p => p.text ?? "")
.join("");
messages.push({ role: "user", content: text || "" });
}
}
Step[] items look like { type: "user_input", content: [...] } — no role key, so they hit the else branch; type !== "text" at the top level, so text is "". The synthetic message becomes { role: "user", content: "" }, fixture matching by userMessage fails, request returns 404.
Why the live API needs Step[]
Per Google's Interactions API docs, input accepts a list of Steps where each step is one of:
Sending raw Array<Content> (the shape aimock's else branch implicitly assumes) produces a live-API invalid_request with "value at top-level must be a list". Sending Array<Turn> works on some SDK paths but not all.
The @google/genai SDK's TypeScript type union (string | Array<Content> | Array<Turn> | TextContent | ...) does not include Array<Step> — the type is incomplete relative to what the live API requires. This means every client that follows the live wire contract sends Step[] and is invisible to aimock.
Repro
import { LLMock } from '@copilotkit/aimock'
const mock = new LLMock({ port: 4010, host: '127.0.0.1' })
// Equivalent of a JSON fixture: { match: { userMessage: 'hi' }, response: { content: 'hello' } }
await mock.loadFixture({ match: { userMessage: 'hi' }, response: { content: 'hello' } })
await mock.start()
await fetch('http://127.0.0.1:4010/v1beta/interactions', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
model: 'gemini-2.5-flash',
stream: true,
input: [
{ type: 'user_input', content: [{ type: 'text', text: 'hi' }] },
],
}),
})
// → 404 { error: { code: "NOT_FOUND", message: "No fixture matched" } }
Swap input for 'hi' and it matches as expected.
Suggested fix
Add a Step[] branch in geminiInteractionsToCompletionRequest. Sketch:
else if (firstItem && typeof firstItem.type === 'string' &&
(firstItem.type === 'user_input' || firstItem.type === 'function_result' /* … */)) {
for (const step of req.input) {
if (step.type === 'user_input') {
const text = (step.content ?? [])
.filter(p => p.type === 'text')
.map(p => p.text ?? '').join('')
messages.push({ role: 'user', content: text })
} else if (step.type === 'function_result') {
messages.push({
role: 'tool',
content: typeof step.result === 'string' ? step.result : JSON.stringify(step.result ?? ''),
tool_call_id: step.call_id ?? step.id ?? '',
})
}
// tool_call / model_output / etc. as needed
}
}
The discriminator (no role + type === 'user_input' | 'function_result' | …) reliably separates Step[] from the existing ContentBlock[] fallback.
Impact
- TanStack AI's
@tanstack/ai-gemini/experimental geminiTextInteractions() adapter sends Step[] (because it's what the live API accepts). E2E tests that exercise it against aimock all 404.
- Any other client built to the live
/v1beta/interactions wire contract has the same problem.
Versions
@copilotkit/aimock@1.26.1 (confirmed identical input branch through 1.24.1 → 1.25.0 → 1.26.0 → 1.26.1)
@google/genai@1.43.0
Happy to send a PR.
Summary
handleGeminiInteractionsinsrc/gemini-interactions.tsdoesn't recognize the Step[] envelope shape that the live Gemini Interactions API requires at the top level ofinput. Clients sending the format Google's/v1beta/interactionsactually accepts get an emptyuserMessageand 404"No fixture matched".What aimock currently handles
geminiInteractionsToCompletionRequest(dist/gemini-interactions.cjs:9) covers three input shapes:Step[] items look like
{ type: "user_input", content: [...] }— norolekey, so they hit theelsebranch;type !== "text"at the top level, sotextis"". The synthetic message becomes{ role: "user", content: "" }, fixture matching byuserMessagefails, request returns 404.Why the live API needs Step[]
Per Google's Interactions API docs,
inputaccepts a list of Steps where each step is one of:{ "type": "user_input", "content": [ { "type": "text", "text": "…" } ] } { "type": "function_result", "call_id": "…", "name": "…", "result": "…" } // also: tool_call, model_output, etc. — see the Step union.Sending raw
Array<Content>(the shape aimock'selsebranch implicitly assumes) produces a live-APIinvalid_requestwith"value at top-level must be a list". SendingArray<Turn>works on some SDK paths but not all.The
@google/genaiSDK's TypeScript type union (string | Array<Content> | Array<Turn> | TextContent | ...) does not includeArray<Step>— the type is incomplete relative to what the live API requires. This means every client that follows the live wire contract sends Step[] and is invisible to aimock.Repro
Swap
inputfor'hi'and it matches as expected.Suggested fix
Add a Step[] branch in
geminiInteractionsToCompletionRequest. Sketch:The discriminator (no
role+type === 'user_input' | 'function_result' | …) reliably separates Step[] from the existingContentBlock[]fallback.Impact
@tanstack/ai-gemini/experimentalgeminiTextInteractions()adapter sends Step[] (because it's what the live API accepts). E2E tests that exercise it against aimock all 404./v1beta/interactionswire contract has the same problem.Versions
@copilotkit/aimock@1.26.1(confirmed identical input branch through 1.24.1 → 1.25.0 → 1.26.0 → 1.26.1)@google/genai@1.43.0Happy to send a PR.