@@ -8,6 +8,8 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed OpenAI-compatible completion usage parsing to stop double-counting reasoning tokens already included in `completion_tokens` ([#3581](https://github.com/badlogic/pi-mono/issues/3581))
|
||||
- Fixed long cache retention compatibility by adding `compat.supportsLongCacheRetention`, allowing Anthropic Messages and OpenAI-compatible proxies to explicitly disable long-retention fields while enabling long retention by default when requested ([#3543](https://github.com/badlogic/pi-mono/issues/3543))
|
||||
- Fixed `openai-responses` compatibility by adding `compat.sendSessionIdHeader: false`, allowing strict OpenAI-compatible proxies to omit the underscore-containing `session_id` header while still sending other session-affinity headers ([#3579](https://github.com/badlogic/pi-mono/issues/3579))
|
||||
- Fixed `anthropic-messages` tool streaming compatibility by adding `compat.supportsEagerToolInputStreaming`, allowing Anthropic-compatible providers to omit per-tool `eager_input_streaming` and use the legacy fine-grained tool streaming beta header instead ([#3575](https://github.com/badlogic/pi-mono/issues/3575))
|
||||
- Fixed `supportsXhigh()` to recognize `openai-codex` `gpt-5.5`, preserving `xhigh` reasoning requests instead of clamping them to `high`.
|
||||
|
||||
@@ -465,15 +465,18 @@ function buildParams(
|
||||
cacheRetention: CacheRetention = resolveCacheRetention(options?.cacheRetention),
|
||||
) {
|
||||
const messages = convertMessages(model, context, compat);
|
||||
const cacheControl = getCompatCacheControl(model, compat, cacheRetention);
|
||||
const cacheControl = getCompatCacheControl(compat, cacheRetention);
|
||||
|
||||
const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||
model: model.id,
|
||||
messages,
|
||||
stream: true,
|
||||
prompt_cache_key:
|
||||
model.baseUrl.includes("api.openai.com") && cacheRetention !== "none" ? options?.sessionId : undefined,
|
||||
prompt_cache_retention: model.baseUrl.includes("api.openai.com") && cacheRetention === "long" ? "24h" : undefined,
|
||||
(model.baseUrl.includes("api.openai.com") && cacheRetention !== "none") ||
|
||||
(cacheRetention === "long" && compat.supportsLongCacheRetention)
|
||||
? options?.sessionId
|
||||
: undefined,
|
||||
prompt_cache_retention: cacheRetention === "long" && compat.supportsLongCacheRetention ? "24h" : undefined,
|
||||
};
|
||||
|
||||
if (compat.supportsUsageInStreaming !== false) {
|
||||
@@ -565,7 +568,6 @@ function mapReasoningEffort(
|
||||
}
|
||||
|
||||
function getCompatCacheControl(
|
||||
model: Model<"openai-completions">,
|
||||
compat: ResolvedOpenAICompletionsCompat,
|
||||
cacheRetention: CacheRetention,
|
||||
): OpenAICompatCacheControl | undefined {
|
||||
@@ -573,7 +575,7 @@ function getCompatCacheControl(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const ttl = cacheRetention === "long" && model.baseUrl.includes("api.anthropic.com") ? "1h" : undefined;
|
||||
const ttl = cacheRetention === "long" && compat.supportsLongCacheRetention ? "1h" : undefined;
|
||||
return { type: "ephemeral", ...(ttl ? { ttl } : {}) };
|
||||
}
|
||||
|
||||
@@ -937,14 +939,12 @@ function parseChunkUsage(
|
||||
prompt_tokens?: number;
|
||||
completion_tokens?: number;
|
||||
prompt_tokens_details?: { cached_tokens?: number; cache_write_tokens?: number };
|
||||
completion_tokens_details?: { reasoning_tokens?: number };
|
||||
},
|
||||
model: Model<"openai-completions">,
|
||||
): AssistantMessage["usage"] {
|
||||
const promptTokens = rawUsage.prompt_tokens || 0;
|
||||
const reportedCachedTokens = rawUsage.prompt_tokens_details?.cached_tokens || 0;
|
||||
const cacheWriteTokens = rawUsage.prompt_tokens_details?.cache_write_tokens || 0;
|
||||
const reasoningTokens = rawUsage.completion_tokens_details?.reasoning_tokens || 0;
|
||||
|
||||
// Normalize to pi-ai semantics:
|
||||
// - cacheRead: hits from cache created by previous requests only
|
||||
@@ -955,9 +955,8 @@ function parseChunkUsage(
|
||||
cacheWriteTokens > 0 ? Math.max(0, reportedCachedTokens - cacheWriteTokens) : reportedCachedTokens;
|
||||
|
||||
const input = Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens);
|
||||
// Compute totalTokens ourselves since we add reasoning_tokens to output
|
||||
// and some providers (e.g., Groq) don't include them in total_tokens
|
||||
const outputTokens = (rawUsage.completion_tokens || 0) + reasoningTokens;
|
||||
// OpenAI completion_tokens already includes reasoning_tokens.
|
||||
const outputTokens = rawUsage.completion_tokens || 0;
|
||||
const usage: AssistantMessage["usage"] = {
|
||||
input,
|
||||
output: outputTokens,
|
||||
@@ -1055,6 +1054,7 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
|
||||
supportsStrictMode: true,
|
||||
cacheControlFormat,
|
||||
sendSessionAffinityHeaders: false,
|
||||
supportsLongCacheRetention: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1084,5 +1084,6 @@ function getCompat(model: Model<"openai-completions">): ResolvedOpenAICompletion
|
||||
supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode,
|
||||
cacheControlFormat: model.compat.cacheControlFormat ?? detected.cacheControlFormat,
|
||||
sendSessionAffinityHeaders: model.compat.sendSessionAffinityHeaders ?? detected.sendSessionAffinityHeaders,
|
||||
supportsLongCacheRetention: model.compat.supportsLongCacheRetention ?? detected.supportsLongCacheRetention,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -552,6 +552,41 @@ describe("openai-completions tool_choice", () => {
|
||||
expect(toolCall).not.toHaveProperty("partialArgs");
|
||||
});
|
||||
|
||||
it("does not double-count reasoning tokens in completion usage", async () => {
|
||||
mockState.chunks = [
|
||||
{
|
||||
id: "chatcmpl-reasoning-usage",
|
||||
choices: [{ delta: {}, finish_reason: "stop" }],
|
||||
usage: {
|
||||
prompt_tokens: 10,
|
||||
completion_tokens: 33,
|
||||
prompt_tokens_details: { cached_tokens: 0 },
|
||||
completion_tokens_details: { reasoning_tokens: 21 },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const { compat: _compat, ...baseModel } = getModel("openai", "gpt-4o-mini")!;
|
||||
const model = { ...baseModel, api: "openai-completions" } as const;
|
||||
const response = await streamSimple(
|
||||
model,
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "Use reasoning.",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{ apiKey: "test" },
|
||||
).result();
|
||||
|
||||
expect(response.usage.input).toBe(10);
|
||||
expect(response.usage.output).toBe(33);
|
||||
expect(response.usage.totalTokens).toBe(43);
|
||||
});
|
||||
|
||||
it("preserves prompt_tokens_details.cache_write_tokens from chunk usage", async () => {
|
||||
mockState.chunks = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user