fix(core): harden semantic context estimation
This commit is contained in:
parent
a48d38b6cd
commit
2acf08cca0
2 changed files with 63 additions and 8 deletions
|
|
@ -80,22 +80,57 @@ export interface Interface {
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionCompaction") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionCompaction") {}
|
||||||
|
|
||||||
const stringify = (value: unknown) => (typeof value === "string" ? value : (JSON.stringify(value) ?? String(value)))
|
const serializeString = (value: unknown) => {
|
||||||
|
try {
|
||||||
|
return String(value)
|
||||||
|
} catch {
|
||||||
|
return "[unserializable]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serializeJson = (value: unknown) => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value) ?? serializeString(value)
|
||||||
|
} catch {
|
||||||
|
return serializeString(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serializeError = (value: unknown) => {
|
||||||
|
try {
|
||||||
|
const prototype =
|
||||||
|
typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value)
|
||||||
|
const structured = Array.isArray(value) || prototype === Object.prototype || prototype === null
|
||||||
|
return structured ? serializeJson(value) : serializeString(value)
|
||||||
|
} catch {
|
||||||
|
return serializeString(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const serializeContent = (part: LLMRequest["messages"][number]["content"][number]) => {
|
const serializeContent = (part: LLMRequest["messages"][number]["content"][number]) => {
|
||||||
if (part.type === "text" || part.type === "reasoning") return part.text
|
if (part.type === "text" || part.type === "reasoning") return part.text
|
||||||
if (part.type === "media") return ""
|
if (part.type === "media") return ""
|
||||||
if (part.type === "tool-call") return `${part.name}\n${stringify(part.input)}`
|
if (part.type === "tool-call") return `${part.name}\n${serializeJson(part.input)}`
|
||||||
|
// OpenAI replays hosted image generations by item reference; the opaque JSON result contains the image bytes.
|
||||||
|
if (
|
||||||
|
part.providerExecuted &&
|
||||||
|
part.name === "image_generation" &&
|
||||||
|
part.result.type === "json" &&
|
||||||
|
typeof part.providerMetadata?.openai?.itemId === "string"
|
||||||
|
)
|
||||||
|
return part.name
|
||||||
if (part.result.type === "content")
|
if (part.result.type === "content")
|
||||||
return [part.name, ...part.result.value.flatMap((item) => (item.type === "text" ? [item.text] : []))].join("\n")
|
return [part.name, ...part.result.value.flatMap((item) => (item.type === "text" ? [item.text] : []))].join("\n")
|
||||||
return `${part.name}\n${stringify(part.result.value)}`
|
if (part.result.type === "text") return `${part.name}\n${serializeString(part.result.value)}`
|
||||||
|
if (part.result.type === "error") return `${part.name}\n${serializeError(part.result.value)}`
|
||||||
|
return `${part.name}\n${serializeJson(part.result.value)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const estimate = (request: LLMRequest) =>
|
const estimate = (request: LLMRequest) =>
|
||||||
Token.estimate(
|
Token.estimate(
|
||||||
[
|
[
|
||||||
...request.system.map((part) => part.text),
|
...request.system.map((part) => part.text),
|
||||||
JSON.stringify(request.tools),
|
serializeJson(request.tools),
|
||||||
...request.messages.flatMap((message) => message.content.map(serializeContent).filter(Boolean)),
|
...request.messages.flatMap((message) => message.content.map(serializeContent).filter(Boolean)),
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,15 @@
|
||||||
import { expect, test } from "bun:test"
|
import { expect, test } from "bun:test"
|
||||||
import { LLM, LLMClient, LLMEvent, Message, Model, ToolCallPart, type LLMRequest } from "@opencode-ai/llm"
|
import {
|
||||||
import { OpenAIChat } from "@opencode-ai/llm/protocols"
|
LLM,
|
||||||
|
LLMClient,
|
||||||
|
LLMEvent,
|
||||||
|
Message,
|
||||||
|
Model,
|
||||||
|
ToolCallPart,
|
||||||
|
ToolResultPart,
|
||||||
|
type LLMRequest,
|
||||||
|
} from "@opencode-ai/llm"
|
||||||
|
import { OpenAIChat, OpenAIResponses } from "@opencode-ai/llm/protocols"
|
||||||
import { Base64, FileAttachment } from "@opencode-ai/schema/prompt"
|
import { Base64, FileAttachment } from "@opencode-ai/schema/prompt"
|
||||||
import { Config } from "@opencode-ai/core/config"
|
import { Config } from "@opencode-ai/core/config"
|
||||||
import { Database } from "@opencode-ai/core/database/database"
|
import { Database } from "@opencode-ai/core/database/database"
|
||||||
|
|
@ -87,7 +96,7 @@ it.effect("does not count image attachments as text context", () =>
|
||||||
const inputModel = Model.make({
|
const inputModel = Model.make({
|
||||||
id: "media-model",
|
id: "media-model",
|
||||||
provider: "test",
|
provider: "test",
|
||||||
route: OpenAIChat.route.with({ limits: { context: 30_000, output: 1_000 } }),
|
route: OpenAIResponses.route.with({ limits: { context: 30_000, output: 1_000 } }),
|
||||||
})
|
})
|
||||||
const inputModelRef = ModelV2.Ref.make({
|
const inputModelRef = ModelV2.Ref.make({
|
||||||
id: ModelV2.ID.make(inputModel.id),
|
id: ModelV2.ID.make(inputModel.id),
|
||||||
|
|
@ -110,7 +119,18 @@ it.effect("does not count image attachments as text context", () =>
|
||||||
]
|
]
|
||||||
const request = LLM.request({
|
const request = LLM.request({
|
||||||
model: inputModel,
|
model: inputModel,
|
||||||
messages: toLLMMessages(messages, inputModelRef),
|
messages: [
|
||||||
|
...toLLMMessages(messages, inputModelRef),
|
||||||
|
Message.assistant(
|
||||||
|
ToolResultPart.make({
|
||||||
|
id: "image_generation_1",
|
||||||
|
name: "image_generation",
|
||||||
|
result: { type: "image_generation_call", output: Buffer.alloc(64 * 1024).toString("base64") },
|
||||||
|
providerExecuted: true,
|
||||||
|
providerMetadata: { openai: { itemId: "image_generation_1" } },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(request.messages.flatMap((message) => message.content)).toContainEqual({
|
expect(request.messages.flatMap((message) => message.content)).toContainEqual({
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue