feat(ai): expose routed model on openai-completions (#3968)

Adds `AssistantMessage.responseModel` on the openai-completions path:
surfaces the concrete `chunk.model` when it differs from the requested
id (e.g. OpenRouter `auto` -> `anthropic/...`).
This commit is contained in:
Alejandro
2026-04-30 00:16:12 +02:00
committed by GitHub
parent cf5ec23240
commit e354c521f4
4 changed files with 148 additions and 0 deletions

View File

@@ -2,6 +2,10 @@
## [Unreleased]
### Added
- Added `AssistantMessage.responseModel` on the openai-completions path: surfaces the concrete `chunk.model` when it differs from the requested id (e.g. OpenRouter `auto` -> `anthropic/...`).
### Fixed
- Fixed DeepSeek V4 Flash `xhigh` thinking support so requests preserve `xhigh` and map it to DeepSeek's `max` reasoning effort ([#3944](https://github.com/badlogic/pi-mono/issues/3944)).

View File

@@ -207,6 +207,9 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
// OpenAI documents ChatCompletionChunk.id as the unique chat completion identifier,
// and each chunk in a streamed completion carries the same id.
output.responseId ||= chunk.id;
if (typeof chunk.model === "string" && chunk.model.length > 0 && chunk.model !== model.id) {
output.responseModel ||= chunk.model;
}
if (chunk.usage) {
output.usage = parseChunkUsage(chunk.usage, model);
}

View File

@@ -216,6 +216,7 @@ export interface AssistantMessage {
api: Api;
provider: Provider;
model: string;
responseModel?: string; // Concrete `chunk.model` when different from the requested `model` (e.g. OpenRouter `auto` -> `anthropic/...`)
responseId?: string; // Provider-specific response/message identifier when the upstream API exposes one
usage: Usage;
stopReason: StopReason;

View File

@@ -0,0 +1,140 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { complete } from "../src/stream.js";
import type { Model } from "../src/types.js";
// Router/virtual ids (e.g. OpenRouter `auto`) keep `model` pinned to the
// requested id and surface the routed concrete id on `responseModel`.
const mockState = vi.hoisted(() => ({
chunks: [] as unknown[],
}));
vi.mock("openai", () => {
class FakeOpenAI {
chat = {
completions: {
create: () => {
const chunks = mockState.chunks;
const stream = {
async *[Symbol.asyncIterator]() {
for (const chunk of chunks) yield chunk;
},
};
const promise = Promise.resolve(stream) as Promise<typeof stream> & {
withResponse: () => Promise<{
data: typeof stream;
response: { status: number; headers: Headers };
}>;
};
promise.withResponse = async () => ({
data: stream,
response: { status: 200, headers: new Headers() },
});
return promise;
},
},
};
}
return { default: FakeOpenAI };
});
function openRouterAuto(): Model<"openai-completions"> {
return {
id: "openrouter/auto",
name: "OpenRouter Auto",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200_000,
maxTokens: 8192,
};
}
describe("openai-completions responseModel", () => {
beforeEach(() => {
mockState.chunks = [];
});
it("surfaces routed chunk.model on responseModel without changing model", async () => {
mockState.chunks = [
{ id: "chatcmpl-1", model: "anthropic/claude-opus-4.7", choices: [{ index: 0, delta: { content: "hi" } }] },
{
id: "chatcmpl-1",
model: "anthropic/claude-opus-4.7",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
usage: {
prompt_tokens: 10,
completion_tokens: 5,
prompt_tokens_details: { cached_tokens: 0 },
completion_tokens_details: { reasoning_tokens: 0 },
},
},
];
const message = await complete(
openRouterAuto(),
{ messages: [{ role: "user", content: "hi", timestamp: Date.now() }] },
{ apiKey: "test" },
);
expect(message.model).toBe("openrouter/auto");
expect(message.responseModel).toBe("anthropic/claude-opus-4.7");
expect(message.provider).toBe("openrouter");
expect(message.stopReason).toBe("stop");
});
it("leaves responseModel undefined when chunks echo the requested id", async () => {
mockState.chunks = [
{ id: "chatcmpl-2", model: "openrouter/auto", choices: [{ index: 0, delta: { content: "hi" } }] },
{
id: "chatcmpl-2",
model: "openrouter/auto",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
usage: {
prompt_tokens: 1,
completion_tokens: 1,
prompt_tokens_details: { cached_tokens: 0 },
completion_tokens_details: { reasoning_tokens: 0 },
},
},
];
const message = await complete(
openRouterAuto(),
{ messages: [{ role: "user", content: "hi", timestamp: Date.now() }] },
{ apiKey: "test" },
);
expect(message.model).toBe("openrouter/auto");
expect(message.responseModel).toBeUndefined();
});
it("ignores empty or missing chunk.model", async () => {
mockState.chunks = [
{ id: "chatcmpl-3", choices: [{ index: 0, delta: { content: "hi" } }] },
{ id: "chatcmpl-3", model: "", choices: [{ index: 0, delta: { content: "!" } }] },
{
id: "chatcmpl-3",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
usage: {
prompt_tokens: 1,
completion_tokens: 2,
prompt_tokens_details: { cached_tokens: 0 },
completion_tokens_details: { reasoning_tokens: 0 },
},
},
];
const message = await complete(
openRouterAuto(),
{ messages: [{ role: "user", content: "hi", timestamp: Date.now() }] },
{ apiKey: "test" },
);
expect(message.model).toBe("openrouter/auto");
expect(message.responseModel).toBeUndefined();
});
});