Merge branch 'codex/invariant-package-registration-gate' into codex/package-invariant-checks
# Conflicts: # .agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.i18n.yaml # .agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md # .agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.zh.md # .agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.i18n.yaml # docs/rfc/INDEX.md # packages/AGENTS.md
This commit is contained in:
@@ -9,4 +9,4 @@ The LLM seam and its provider adapters. The interface package (`llm`) owns the a
|
||||
| `llm-deepseek/` | DeepSeek API adapter (hand-rolled fetch/SSE) | (registers on `ctx.llm`) |
|
||||
| `llm-pi-ai/` | Multi-provider adapter via `@earendil-works/pi-ai` | (registers on `ctx.llm`) |
|
||||
|
||||
The interface lives at `llm/llm/`; adapters and the reusable token meter are flat siblings under the group. Requests route by `provider`, while `model` is passed through to the selected adapter. A new provider adapter joins here and registers one or more provider routes on `ctx.llm` without touching the interface. See [twin LLM adapters](../../docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.md) for the contract-validation origin of the two shipping implementations and the [replay token meter RFC](../../docs/rfc/implemented/architecture/2026-07-15-replay-token-meter-service.md) for measurement ownership.
|
||||
The interface lives at `llm/llm/`; adapters and the reusable token meter are flat siblings under the group. Requests route by `provider`, while `model` is passed through to the selected adapter. A new provider adapter joins here and registers one or more provider routes on `ctx.llm` without touching the interface. See [twin LLM adapters](../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md) for the contract-validation origin of the two shipping implementations and the [replay token meter Agent Note](../../.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.md) for measurement ownership.
|
||||
|
||||
@@ -52,15 +52,31 @@ Unit suites run against a local `node:http` mock SSE server (no network). Real-A
|
||||
|
||||
### DeepSeek request
|
||||
|
||||
**What the model sees**: The selected DeepSeek model receives the harness system prompt, message history, tool schemas, stop sequences, and call config without adapter-authored prompt prose. On a prior assistant turn with tool calls, its reasoning content is passed back as required; reasoning from tool-call-free turns is omitted.
|
||||
#### What the model sees
|
||||
|
||||
**Token effect**: Provider tokenization governs exact input. Conditional reasoning passback increases tool-round-trip context, while dropping other reasoning avoids paying those tokens again; cache-read usage is reported when available.
|
||||
The selected DeepSeek model receives the harness system prompt, message history, tool schemas, stop sequences, and call config without adapter-authored prompt prose. On a prior assistant turn with tool calls, its reasoning content is passed back as required; reasoning from tool-call-free turns is omitted.
|
||||
|
||||
#### Token effect
|
||||
|
||||
Provider tokenization governs exact input. Conditional reasoning passback increases tool-round-trip context, while dropping other reasoning avoids paying those tokens again; cache-read usage is reported when available.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
An unchanged assembled prefix is eligible for DeepSeek cache reuse, which this adapter reports in usage. A model-route change or any upstream prompt, schema, prefix, or history change may prevent reuse from the first changed token; reasoning passback appends during tool round trips.
|
||||
|
||||
### DeepSeek response
|
||||
|
||||
**What the model sees**: Reasoning, text, and raw-string tool arguments are translated into harness chunks for the loop to log and assemble.
|
||||
#### What the model sees
|
||||
|
||||
**Token effect**: Generated tokens follow provider thinking and effort settings plus the request's `maxTokens`; only loop-retained blocks affect later input.
|
||||
Reasoning, text, and raw-string tool arguments are translated into harness chunks for the loop to log and assemble.
|
||||
|
||||
#### Token effect
|
||||
|
||||
Generated tokens follow provider thinking and effort settings plus the request's `maxTokens`; only loop-retained blocks affect later input.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
Loop-retained response blocks append to the next request and preserve its earlier reusable prefix; dropped blocks have no later cache effect. Changing the provider or model selects a different cache domain.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
|
||||
@@ -91,6 +91,9 @@ export class DeepSeekAdapter extends LlmAdapter {
|
||||
'content-type': 'application/json',
|
||||
'accept': 'text/event-stream',
|
||||
...attributionHeaders(),
|
||||
...options.sessionId !== undefined
|
||||
? { 'x-deepseek-harness-session-id': String(options.sessionId) }
|
||||
: {},
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
...options.signal ? { signal: options.signal } : {},
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { IncomingMessage, Server, ServerResponse } from 'node:http'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import LlmService, { CONTEXT_WINDOW_EXCEEDED_CODE, LlmError, userAgent } from '@deepseek-ai/dsh-llm'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import { DeepSeekAdapter } from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import { httpErrorCode } from '../src/adapter.ts'
|
||||
@@ -133,6 +134,19 @@ describe('DeepSeekAdapter against a mock server', () => {
|
||||
expect(kinds).toEqual(['block-start', 'text-delta', 'block-end', 'usage', 'finish'])
|
||||
})
|
||||
|
||||
it('forwards the harness session id for host-side trajectory routing', async () => {
|
||||
const server = await mockServer([{ kind: 'sse', events: textEvents }])
|
||||
const ctx = await harness(server.url)
|
||||
|
||||
await assemble(ctx, {
|
||||
model: 'deepseek-v4-flash',
|
||||
messages: [{ role: 'user', content: [{ type: 'text', text: 'hi' }] }],
|
||||
sessionId: SessionId('child-session'),
|
||||
})
|
||||
|
||||
expect(server.headers[0]?.['x-deepseek-harness-session-id']).toBe('child-session')
|
||||
})
|
||||
|
||||
it('forwards thinking config onto the wire', async () => {
|
||||
const server = await mockServer([{ kind: 'sse', events: textEvents }])
|
||||
const ctx = await harness(server.url, { thinking: 'disabled', reasoningEffort: 'high' })
|
||||
|
||||
@@ -63,15 +63,31 @@ Unit tests use pi-ai catalog models redirected to local mock servers and cover p
|
||||
|
||||
### Provider request through pi-ai
|
||||
|
||||
**What the model sees**: The selected catalog model receives `GenerateOptions.system`, history, tools, and sampling fields supported by pi-ai's common streaming API. This package adds no prompt prose. Provider-native replay metadata is restored only when the adapter validates it for the historical content.
|
||||
#### What the model sees
|
||||
|
||||
**Token effect**: Provider tokenization governs exact input. Conversion adds no model-visible text; replay metadata may let a native API reuse provider-side state.
|
||||
The selected catalog model receives `GenerateOptions.system`, history, tools, and sampling fields supported by pi-ai's common streaming API. This package adds no prompt prose. Provider-native replay metadata is restored only when the adapter validates it for the historical content.
|
||||
|
||||
#### Token effect
|
||||
|
||||
Provider tokenization governs exact input. Conversion adds no model-visible text; replay metadata may let a native API reuse provider-side state.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
Conversion preserves logical request order without adding text, while the selected provider's serialization and replay state determine reuse. Changing adapter instance, provider, model, or any upstream request token may prevent reuse from the first difference.
|
||||
|
||||
### Provider response
|
||||
|
||||
**What the model sees**: pi-ai events become harness reasoning, text, tool-call, usage, and finish chunks. Parsed tool arguments cross the harness boundary as raw JSON strings.
|
||||
#### What the model sees
|
||||
|
||||
**Token effect**: Generated content affects later inputs only after the loop records it. pi-ai folds reasoning tokens into output usage when the provider does not report them separately.
|
||||
pi-ai events become harness reasoning, text, tool-call, usage, and finish chunks. Parsed tool arguments cross the harness boundary as raw JSON strings.
|
||||
|
||||
#### Token effect
|
||||
|
||||
Generated content affects later inputs only after the loop records it. pi-ai folds reasoning tokens into output usage when the provider does not report them separately.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
Recorded response content appends to the next request and does not invalidate its earlier reusable prefix. Unrecorded transport metadata and usage accounting do not affect cache identity.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
|
||||
@@ -168,6 +168,26 @@ describe('PiAiAdapter provider routing', () => {
|
||||
expect(server.paths).toEqual(['/v1/responses'])
|
||||
})
|
||||
|
||||
it('uses OpenAI Responses against an Azure project v1 path with its API key header', async () => {
|
||||
const server = await mockServer([{ status: 401, body: JSON.stringify({ error: { message: 'expected mock failure' } }) }])
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmPiAi, {
|
||||
providers: [{
|
||||
provider: 'openai',
|
||||
apiKey: 'test-key',
|
||||
baseURL: `${server.url}/api/projects/openai/openai/v1`,
|
||||
headers: { 'api-key': 'test-key', Authorization: '' },
|
||||
maxRetries: 0,
|
||||
}],
|
||||
})
|
||||
const result = await assemble(ctx, { provider: 'openai', model: 'gpt-5.5', messages: [] })
|
||||
expect(result.finish.kind).toBe('error')
|
||||
expect(server.paths).toEqual(['/api/projects/openai/openai/v1/responses'])
|
||||
expect(server.headers[0]?.['api-key']).toBe('test-key')
|
||||
expect(server.headers[0]?.authorization).toBe('')
|
||||
})
|
||||
|
||||
it.each([
|
||||
[401, 'AUTH'],
|
||||
[400, 'INVALID_REQUEST'],
|
||||
|
||||
163
packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts
Normal file
163
packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import LlmService, { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import type { Message, ToolSchema } from '@deepseek-ai/dsh-llm'
|
||||
import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai'
|
||||
import type { PiAiReplayState } from '../src/replay.ts'
|
||||
import { assemble, type AssembledResult } from './assemble.ts'
|
||||
|
||||
interface ProviderCase {
|
||||
provider: 'openai' | 'anthropic'
|
||||
api: 'openai-responses' | 'anthropic-messages'
|
||||
model: string
|
||||
apiKey?: string
|
||||
baseURL?: string
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
|
||||
const openAIBaseURL = process.env.DSH_PI_AI_OPENAI_BASE_URL
|
||||
const azureOpenAIKey = process.env.AZURE_OPENAI_API_KEY
|
||||
|
||||
const providerCases: ProviderCase[] = [
|
||||
{
|
||||
provider: 'openai',
|
||||
api: 'openai-responses',
|
||||
model: process.env.DSH_PI_AI_OPENAI_MODEL ?? 'gpt-5.5',
|
||||
...azureOpenAIKey
|
||||
? { apiKey: azureOpenAIKey, headers: { 'api-key': azureOpenAIKey, Authorization: '' } }
|
||||
: {},
|
||||
...openAIBaseURL ? { baseURL: openAIBaseURL } : {},
|
||||
},
|
||||
{
|
||||
provider: 'anthropic',
|
||||
api: 'anthropic-messages',
|
||||
model: process.env.DSH_PI_AI_ANTHROPIC_MODEL ?? 'claude-opus-4-8',
|
||||
...process.env.ANTHROPIC_API_KEY ? { apiKey: process.env.ANTHROPIC_API_KEY } : {},
|
||||
},
|
||||
]
|
||||
|
||||
const contexts: Context[] = []
|
||||
|
||||
async function harness(): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
contexts.push(ctx)
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmPiAi, {
|
||||
providers: providerCases.map(profile => ({
|
||||
provider: profile.provider,
|
||||
...profile.apiKey === undefined ? {} : { apiKey: profile.apiKey },
|
||||
...profile.baseURL === undefined ? {} : { baseURL: profile.baseURL },
|
||||
...profile.headers === undefined ? {} : { headers: profile.headers },
|
||||
})),
|
||||
})
|
||||
return ctx
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
|
||||
})
|
||||
|
||||
function ask(text: string): Message[] {
|
||||
return [{ role: 'user', content: [{ type: 'text', text }] }]
|
||||
}
|
||||
|
||||
function textOf(result: AssembledResult): string {
|
||||
return result.message.content
|
||||
.filter(block => block.type === 'text')
|
||||
.map(block => block.text)
|
||||
.join('')
|
||||
}
|
||||
|
||||
function expectFinish(result: AssembledResult, expected: 'stop' | 'tool-calls'): void {
|
||||
if (result.finish.kind === 'error') {
|
||||
throw new Error(`provider request failed (${result.finish.code ?? 'unknown'}): ${result.finish.message}`)
|
||||
}
|
||||
expect(result.finish.kind).toBe(expected)
|
||||
}
|
||||
|
||||
function expectNativeReplay(result: AssembledResult, profile: ProviderCase): PiAiReplayState {
|
||||
const replayState = result.message.provenance?.replayState
|
||||
expect(replayState).toMatchObject({
|
||||
kind: 'pi-ai',
|
||||
version: 1,
|
||||
api: profile.api,
|
||||
provider: profile.provider,
|
||||
model: profile.model,
|
||||
})
|
||||
return replayState as PiAiReplayState
|
||||
}
|
||||
|
||||
const lookupTool: ToolSchema = {
|
||||
name: 'lookup_code',
|
||||
description: 'Look up the word represented by a short code.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: { code: { type: 'string', description: 'The code to look up.' } },
|
||||
required: ['code'],
|
||||
},
|
||||
}
|
||||
|
||||
for (const profile of providerCases) {
|
||||
describe.skipIf(profile.apiKey === undefined)(
|
||||
`llm-pi-ai ${profile.provider} e2e (${profile.api})`,
|
||||
() => {
|
||||
it('streams text with usage and native replay metadata', async () => {
|
||||
const ctx = await harness()
|
||||
const result = await assemble(ctx, {
|
||||
provider: profile.provider,
|
||||
model: profile.model,
|
||||
messages: ask('Reply with exactly the word: pong'),
|
||||
maxTokens: 1024,
|
||||
})
|
||||
|
||||
expectFinish(result, 'stop')
|
||||
expect(textOf(result).toLowerCase()).toContain('pong')
|
||||
expect(result.usage?.inputTokens).toBeGreaterThan(0)
|
||||
expect(result.usage?.outputTokens).toBeGreaterThan(0)
|
||||
expect(expectNativeReplay(result, profile).stopReason).toBe('stop')
|
||||
})
|
||||
|
||||
it('round-trips a tool call with provider-native replay metadata', async () => {
|
||||
const ctx = await harness()
|
||||
const prompt = ask('Use lookup_code with code "blue". Do not answer without calling the tool.')
|
||||
const first = await assemble(ctx, {
|
||||
provider: profile.provider,
|
||||
model: profile.model,
|
||||
messages: prompt,
|
||||
tools: [lookupTool],
|
||||
maxTokens: 2048,
|
||||
})
|
||||
|
||||
expectFinish(first, 'tool-calls')
|
||||
const call = first.message.content.find(block => block.type === 'tool-call')
|
||||
expect(call).toBeDefined()
|
||||
expect(call!.name).toBe('lookup_code')
|
||||
expect(JSON.parse(call!.arguments)).toMatchObject({ code: 'blue' })
|
||||
expect(expectNativeReplay(first, profile).stopReason).toBe('toolUse')
|
||||
|
||||
const second = await assemble(ctx, {
|
||||
provider: profile.provider,
|
||||
model: profile.model,
|
||||
messages: [
|
||||
...prompt,
|
||||
first.message,
|
||||
{
|
||||
role: 'user',
|
||||
content: [{
|
||||
type: 'tool-result',
|
||||
toolCallId: CallId(call!.id),
|
||||
content: [{ type: 'text', text: 'The code blue means ocean.' }],
|
||||
}],
|
||||
},
|
||||
],
|
||||
tools: [lookupTool],
|
||||
maxTokens: 2048,
|
||||
})
|
||||
|
||||
expectFinish(second, 'stop')
|
||||
expect(textOf(second).toLowerCase()).toContain('ocean')
|
||||
expect(expectNativeReplay(second, profile).stopReason).toBe('stop')
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -40,7 +40,7 @@ Streaming is a raw chunk protocol (`block-start`, `text-delta`, `reasoning-delta
|
||||
|
||||
### App attribution (`attribution.ts`)
|
||||
|
||||
Every product adapter sends application identity on provider HTTP requests. `attributionHeaders(identity?)` builds the standard `User-Agent`, defaulting to public `APP_IDENTITY`; white-label deployments may replace but not suppress it. Adapters verify the wire header directly or through their library hook. See [the attribution RFC](../../../docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md).
|
||||
Every product adapter sends application identity on provider HTTP requests. `attributionHeaders(identity?)` builds the standard `User-Agent`, defaulting to public `APP_IDENTITY`; white-label deployments may replace but not suppress it. Adapters verify the wire header directly or through their library hook. See [the attribution Agent Note](../../../.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md).
|
||||
|
||||
### Classes
|
||||
|
||||
@@ -52,17 +52,21 @@ Every product adapter sends application identity on provider HTTP requests. `att
|
||||
|
||||
### Real adapters
|
||||
|
||||
Two adapters implement `LlmAdapter` on different internals: [`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) uses hand-rolled fetch/SSE for the `deepseek` route, while [`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) dynamically resolves configured provider/model pairs through `@earendil-works/pi-ai`. Both follow the `StreamChunk` conventions in `types.ts`: usage precedes finish, tool arguments remain raw strings, and errors take one of two sanctioned paths. See [the twin LLM adapters](../../../docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.md) for the design rationale.
|
||||
Two adapters implement `LlmAdapter` on different internals: [`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) uses hand-rolled fetch/SSE for the `deepseek` route, while [`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) dynamically resolves configured provider/model pairs through `@earendil-works/pi-ai`. Both follow the `StreamChunk` conventions in `types.ts`: usage precedes finish, tool arguments remain raw strings, and errors take one of two sanctioned paths. See [the twin LLM adapters](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md) for the design rationale.
|
||||
|
||||
## Model Experience
|
||||
|
||||
None, as this adapter registry forwards an already assembled request without adding or changing any model-bound text, schema, or message.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
Pass-through; the registry preserves the assembled request prefix, while the selected adapter and provider own cache reuse and routing boundaries.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **No default retry/caching/rate-limit policy ships in this service** — `llm/stream` remains the call-wrapper seam; the agent loop separately offers proven model-request failures to `agent/request-error`, whose default preserves the original failure.
|
||||
- **`GenerateOptions` sampling is `temperature`/`maxTokens`/`stop` only** — no `tool_choice`, `top_p`, or penalty fields; the vocabulary grows when a producer lands ([dropped inert knobs](../../../docs/rfc/implemented/simplification/2026-07-04-drop-inert-request-knobs.md)).
|
||||
- **Producer-gated variants stay out until produced** — `prefill`, per-tool `strict`, block `cache` hints, and the `agent` message-source variant were pruned as producerless ([RFC](../../../docs/rfc/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md)).
|
||||
- **`GenerateOptions` sampling is `temperature`/`maxTokens`/`stop` only** — no `tool_choice`, `top_p`, or penalty fields; the vocabulary grows when a producer lands ([dropped inert knobs](../../../.agents/notes/implemented/simplification/2026-07-04-drop-inert-request-knobs.md)).
|
||||
- **Producer-gated variants stay out until produced** — `prefill`, per-tool `strict`, block `cache` hints, and the `agent` message-source variant were pruned as producerless ([Agent Note](../../../.agents/notes/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md)).
|
||||
- **`BlockAssembler` handles core block kinds only** — a plugin-added block type whose stream is never closed by `block-end` makes `blocks()` throw.
|
||||
- **`APP_IDENTITY.url` names a repository that does not exist yet** — `FIXME`: creating the public `deepseek-ai/deepseek-harness-sdk` repo gates the first release.
|
||||
- **`GenerateOptions.sessionId` is a locally-declared brand** — importing dsh-session's `SessionId` would cycle; a future ids-owning package would dissolve the workaround.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
|
||||
* adapters from drifting. See
|
||||
* `docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
|
||||
* `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
|
||||
*
|
||||
* App-attribution vocabulary for provider requests.
|
||||
* @module @deepseek-ai/dsh-llm/attribution
|
||||
|
||||
@@ -35,7 +35,7 @@ declare module 'cordis' {
|
||||
* adapter's stream, or yield your own chunks to short-circuit.
|
||||
* @param options - the full request. A LOOP-built request arrives
|
||||
* deep-frozen (mutation throws): its content is a pure function of the
|
||||
* session log (the reconstructability RFC), so listeners read it, never
|
||||
* session log (the reconstructability Agent Note), so listeners read it, never
|
||||
* rewrite it. A hand-built one-shot (compaction summarize) is the
|
||||
* caller's own object and stays mutable here.
|
||||
* @mode waterfall
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Property-based tests for the BlockAssembler (the property-testing RFC).
|
||||
* Property-based tests for the BlockAssembler (the property-testing Agent Note).
|
||||
*
|
||||
* The assembler is protocol-shaped: arbitrary interleavings of block-start,
|
||||
* deltas, block-end, usage, and finish — valid and malformed (duplicate
|
||||
|
||||
@@ -42,6 +42,10 @@ Both plugins have usable defaults. A deployment with a different capacity config
|
||||
|
||||
Indirectly, through consumers such as `dsh-compact-basic`; the service itself adds no prompt, message, schema, tool, or model call.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
No direct invalidation; the named consumer owns any request-prefix changes.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **The fixed heuristic is approximate** — content without reusable provider usage is priced by character count plus structural overhead, not an exact provider tokenizer or request serializer.
|
||||
|
||||
Reference in New Issue
Block a user