@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
### Fixed
|
### 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 `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 `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`.
|
- 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),
|
cacheRetention: CacheRetention = resolveCacheRetention(options?.cacheRetention),
|
||||||
) {
|
) {
|
||||||
const messages = convertMessages(model, context, compat);
|
const messages = convertMessages(model, context, compat);
|
||||||
const cacheControl = getCompatCacheControl(model, compat, cacheRetention);
|
const cacheControl = getCompatCacheControl(compat, cacheRetention);
|
||||||
|
|
||||||
const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||||
model: model.id,
|
model: model.id,
|
||||||
messages,
|
messages,
|
||||||
stream: true,
|
stream: true,
|
||||||
prompt_cache_key:
|
prompt_cache_key:
|
||||||
model.baseUrl.includes("api.openai.com") && cacheRetention !== "none" ? options?.sessionId : undefined,
|
(model.baseUrl.includes("api.openai.com") && cacheRetention !== "none") ||
|
||||||
prompt_cache_retention: model.baseUrl.includes("api.openai.com") && cacheRetention === "long" ? "24h" : undefined,
|
(cacheRetention === "long" && compat.supportsLongCacheRetention)
|
||||||
|
? options?.sessionId
|
||||||
|
: undefined,
|
||||||
|
prompt_cache_retention: cacheRetention === "long" && compat.supportsLongCacheRetention ? "24h" : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (compat.supportsUsageInStreaming !== false) {
|
if (compat.supportsUsageInStreaming !== false) {
|
||||||
@@ -565,7 +568,6 @@ function mapReasoningEffort(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCompatCacheControl(
|
function getCompatCacheControl(
|
||||||
model: Model<"openai-completions">,
|
|
||||||
compat: ResolvedOpenAICompletionsCompat,
|
compat: ResolvedOpenAICompletionsCompat,
|
||||||
cacheRetention: CacheRetention,
|
cacheRetention: CacheRetention,
|
||||||
): OpenAICompatCacheControl | undefined {
|
): OpenAICompatCacheControl | undefined {
|
||||||
@@ -573,7 +575,7 @@ function getCompatCacheControl(
|
|||||||
return undefined;
|
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 } : {}) };
|
return { type: "ephemeral", ...(ttl ? { ttl } : {}) };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -937,14 +939,12 @@ function parseChunkUsage(
|
|||||||
prompt_tokens?: number;
|
prompt_tokens?: number;
|
||||||
completion_tokens?: number;
|
completion_tokens?: number;
|
||||||
prompt_tokens_details?: { cached_tokens?: number; cache_write_tokens?: number };
|
prompt_tokens_details?: { cached_tokens?: number; cache_write_tokens?: number };
|
||||||
completion_tokens_details?: { reasoning_tokens?: number };
|
|
||||||
},
|
},
|
||||||
model: Model<"openai-completions">,
|
model: Model<"openai-completions">,
|
||||||
): AssistantMessage["usage"] {
|
): AssistantMessage["usage"] {
|
||||||
const promptTokens = rawUsage.prompt_tokens || 0;
|
const promptTokens = rawUsage.prompt_tokens || 0;
|
||||||
const reportedCachedTokens = rawUsage.prompt_tokens_details?.cached_tokens || 0;
|
const reportedCachedTokens = rawUsage.prompt_tokens_details?.cached_tokens || 0;
|
||||||
const cacheWriteTokens = rawUsage.prompt_tokens_details?.cache_write_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:
|
// Normalize to pi-ai semantics:
|
||||||
// - cacheRead: hits from cache created by previous requests only
|
// - cacheRead: hits from cache created by previous requests only
|
||||||
@@ -955,9 +955,8 @@ function parseChunkUsage(
|
|||||||
cacheWriteTokens > 0 ? Math.max(0, reportedCachedTokens - cacheWriteTokens) : reportedCachedTokens;
|
cacheWriteTokens > 0 ? Math.max(0, reportedCachedTokens - cacheWriteTokens) : reportedCachedTokens;
|
||||||
|
|
||||||
const input = Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens);
|
const input = Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens);
|
||||||
// Compute totalTokens ourselves since we add reasoning_tokens to output
|
// OpenAI completion_tokens already includes reasoning_tokens.
|
||||||
// and some providers (e.g., Groq) don't include them in total_tokens
|
const outputTokens = rawUsage.completion_tokens || 0;
|
||||||
const outputTokens = (rawUsage.completion_tokens || 0) + reasoningTokens;
|
|
||||||
const usage: AssistantMessage["usage"] = {
|
const usage: AssistantMessage["usage"] = {
|
||||||
input,
|
input,
|
||||||
output: outputTokens,
|
output: outputTokens,
|
||||||
@@ -1055,6 +1054,7 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
|
|||||||
supportsStrictMode: true,
|
supportsStrictMode: true,
|
||||||
cacheControlFormat,
|
cacheControlFormat,
|
||||||
sendSessionAffinityHeaders: false,
|
sendSessionAffinityHeaders: false,
|
||||||
|
supportsLongCacheRetention: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1084,5 +1084,6 @@ function getCompat(model: Model<"openai-completions">): ResolvedOpenAICompletion
|
|||||||
supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode,
|
supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode,
|
||||||
cacheControlFormat: model.compat.cacheControlFormat ?? detected.cacheControlFormat,
|
cacheControlFormat: model.compat.cacheControlFormat ?? detected.cacheControlFormat,
|
||||||
sendSessionAffinityHeaders: model.compat.sendSessionAffinityHeaders ?? detected.sendSessionAffinityHeaders,
|
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");
|
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 () => {
|
it("preserves prompt_tokens_details.cache_write_tokens from chunk usage", async () => {
|
||||||
mockState.chunks = [
|
mockState.chunks = [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user