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:
@@ -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)).
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
140
packages/ai/test/openai-completions-response-model.test.ts
Normal file
140
packages/ai/test/openai-completions-response-model.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user