|
@@ -7,7 +7,7 @@
|
|
|
import { Context, Service } from '@deepseek-ai/cordis'
|
|
import { Context, Service } from '@deepseek-ai/cordis'
|
|
|
import z from '@deepseek-ai/schemastery'
|
|
import z from '@deepseek-ai/schemastery'
|
|
|
import { BlockAssembler, deepFreeze } from '@deepseek-ai/dsh-llm'
|
|
import { BlockAssembler, deepFreeze } from '@deepseek-ai/dsh-llm'
|
|
|
-import type { Message, TokenUsage } from '@deepseek-ai/dsh-llm'
|
|
|
|
|
|
|
+import type { LlmImageRequestPricing, Message, TokenUsage } from '@deepseek-ai/dsh-llm'
|
|
|
import type { EpochHeader, Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
|
import type { EpochHeader, Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
|
|
import { canonicalHeader, headerEquals, isSurfaceEvent } from '@deepseek-ai/dsh-session'
|
|
import { canonicalHeader, headerEquals, isSurfaceEvent } from '@deepseek-ai/dsh-session'
|
|
|
// Type-only: resolves the optional projection registry Context declaration.
|
|
// Type-only: resolves the optional projection registry Context declaration.
|
|
@@ -16,27 +16,36 @@ import type {
|
|
|
TokenMeasurement,
|
|
TokenMeasurement,
|
|
|
TokenMeasurementBaseline,
|
|
TokenMeasurementBaseline,
|
|
|
TokenMeterConfig,
|
|
TokenMeterConfig,
|
|
|
- TokenSurfaceNode,
|
|
|
|
|
} from './types.ts'
|
|
} from './types.ts'
|
|
|
import { contextBreakdownProjectionDefinition } from './breakdown-projection.ts'
|
|
import { contextBreakdownProjectionDefinition } from './breakdown-projection.ts'
|
|
|
import { contextPressureProjectionDefinition, tokenUsageProjectionDefinition } from './usage-projection.ts'
|
|
import { contextPressureProjectionDefinition, tokenUsageProjectionDefinition } from './usage-projection.ts'
|
|
|
import { estimateContent, estimateHeader, estimateMessage, ROLE_OVERHEAD } from './estimate.ts'
|
|
import { estimateContent, estimateHeader, estimateMessage, ROLE_OVERHEAD } from './estimate.ts'
|
|
|
import { commitSurfaceTokens, planSurfaceTokens } from './surface-fold.ts'
|
|
import { commitSurfaceTokens, planSurfaceTokens } from './surface-fold.ts'
|
|
|
|
|
+import type { MeterSurfaceNode } from './surface-fold.ts'
|
|
|
|
|
+import { priceSurface } from './route-pricing.ts'
|
|
|
|
|
|
|
|
export type * from './types.ts'
|
|
export type * from './types.ts'
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * Raw anchor facts captured at the latest successful call; the baseline is
|
|
|
|
|
+ * derived per measurement so the anchored surface reprices under the same
|
|
|
|
|
+ * route pricing as the current surface it is compared with.
|
|
|
|
|
+ */
|
|
|
interface MeasurementAnchor {
|
|
interface MeasurementAnchor {
|
|
|
readonly header: EpochHeader | undefined
|
|
readonly header: EpochHeader | undefined
|
|
|
- readonly surfaceTokens: number
|
|
|
|
|
- readonly baseline: Exclude<TokenMeasurementBaseline, { kind: 'none' }>
|
|
|
|
|
|
|
+ /** Surface snapshot the anchored request was derived from. */
|
|
|
|
|
+ readonly nodes: readonly MeterSurfaceNode[]
|
|
|
|
|
+ /** Fixed-heuristic price of the call's provider output. */
|
|
|
|
|
+ readonly assistantTokens: number
|
|
|
|
|
+ /** Provider usage of the call, when it reported one under a known header. */
|
|
|
|
|
+ readonly usage: TokenUsage | undefined
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
interface ReplayState {
|
|
interface ReplayState {
|
|
|
consumedEvents: number
|
|
consumedEvents: number
|
|
|
header: EpochHeader | undefined
|
|
header: EpochHeader | undefined
|
|
|
- surface: TokenSurfaceNode[]
|
|
|
|
|
- surfaceTokens: number
|
|
|
|
|
- stepStart: { turn: number; step: number; surfaceTokens: number } | undefined
|
|
|
|
|
|
|
+ surface: MeterSurfaceNode[]
|
|
|
|
|
+ stepStart: { turn: number; step: number; nodes: readonly MeterSurfaceNode[] } | undefined
|
|
|
anchor: MeasurementAnchor | undefined
|
|
anchor: MeasurementAnchor | undefined
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -100,14 +109,18 @@ export class TokenMeter extends Service {
|
|
|
/**
|
|
/**
|
|
|
* Measure current request pressure and surface through the durable tail.
|
|
* Measure current request pressure and surface through the durable tail.
|
|
|
*
|
|
*
|
|
|
- * Provider usage is reused only when the latest successful call's canonical
|
|
|
|
|
- * request envelope matches `requestHeader` and its total is no lower than
|
|
|
|
|
- * that call's full heuristic anchor; otherwise the complete envelope and
|
|
|
|
|
- * surface are heuristically repriced.
|
|
|
|
|
|
|
+ * The effective envelope's routed provider/model selects the request-image
|
|
|
|
|
+ * pricing every node is priced under: a route whose adapter declares image
|
|
|
|
|
+ * pricing charges each retained image its visual tokens plus its
|
|
|
|
|
+ * model-visible text, while other routes keep the fixed heuristic. Provider
|
|
|
|
|
+ * usage is reused only when the latest successful call's canonical request
|
|
|
|
|
+ * envelope matches `requestHeader` and its total is no lower than that
|
|
|
|
|
+ * call's full route-priced anchor; otherwise the complete envelope and
|
|
|
|
|
+ * surface are repriced.
|
|
|
*
|
|
*
|
|
|
- * `requestHeader` affects request pressure only; surface fields always
|
|
|
|
|
- * describe the current session surface. Every call clones those positional
|
|
|
|
|
- * nodes, so measurement is O(surface).
|
|
|
|
|
|
|
+ * `requestHeader` replaces the latest logged envelope for pressure and node
|
|
|
|
|
+ * pricing; the node set always describes the current session surface. Every
|
|
|
|
|
+ * call clones those positional nodes, so measurement is O(surface).
|
|
|
*
|
|
*
|
|
|
* @param session - session to replay through its current durable tail.
|
|
* @param session - session to replay through its current durable tail.
|
|
|
* @param requestHeader - optional effective request envelope replacing the latest logged header.
|
|
* @param requestHeader - optional effective request envelope replacing the latest logged header.
|
|
@@ -118,20 +131,33 @@ export class TokenMeter extends Service {
|
|
|
const header = requestHeader === undefined
|
|
const header = requestHeader === undefined
|
|
|
? state.header
|
|
? state.header
|
|
|
: canonicalHeader(requestHeader)
|
|
: canonicalHeader(requestHeader)
|
|
|
|
|
+ const pricing = this._routeImagePricing(header)
|
|
|
|
|
+ const surface = priceSurface(state.surface, pricing)
|
|
|
const anchor = state.anchor
|
|
const anchor = state.anchor
|
|
|
|
|
|
|
|
let baseline: TokenMeasurementBaseline
|
|
let baseline: TokenMeasurementBaseline
|
|
|
let surfaceDeltaTokens: number
|
|
let surfaceDeltaTokens: number
|
|
|
if (anchor !== undefined && optionalHeaderEquals(anchor.header, header)) {
|
|
if (anchor !== undefined && optionalHeaderEquals(anchor.header, header)) {
|
|
|
- baseline = anchor.baseline
|
|
|
|
|
- surfaceDeltaTokens = state.surfaceTokens - anchor.surfaceTokens
|
|
|
|
|
- } else if (header === undefined && state.surfaceTokens === 0) {
|
|
|
|
|
|
|
+ // Matching headers share one route, so the anchored snapshot reprices
|
|
|
|
|
+ // under the same pricing as the current surface and the signed delta
|
|
|
|
|
+ // compares like with like.
|
|
|
|
|
+ const anchorSurfaceTokens = priceSurface(anchor.nodes, pricing).surfaceTokens
|
|
|
|
|
+ + anchor.assistantTokens
|
|
|
|
|
+ const estimatedAnchorTokens = estimateHeader(header) + anchorSurfaceTokens
|
|
|
|
|
+ const usage = anchor.usage
|
|
|
|
|
+ // Signed heuristic deltas remain conservative only from an anchor
|
|
|
|
|
+ // that is at least as large as the matching full heuristic price.
|
|
|
|
|
+ baseline = usage !== undefined && usageTokens(usage) >= estimatedAnchorTokens
|
|
|
|
|
+ ? { kind: 'usage', tokens: usageTokens(usage), usage }
|
|
|
|
|
+ : { kind: 'estimated', tokens: estimatedAnchorTokens }
|
|
|
|
|
+ surfaceDeltaTokens = surface.surfaceTokens - anchorSurfaceTokens
|
|
|
|
|
+ } else if (header === undefined && surface.surfaceTokens === 0) {
|
|
|
baseline = { kind: 'none', tokens: 0 }
|
|
baseline = { kind: 'none', tokens: 0 }
|
|
|
surfaceDeltaTokens = 0
|
|
surfaceDeltaTokens = 0
|
|
|
} else {
|
|
} else {
|
|
|
baseline = {
|
|
baseline = {
|
|
|
kind: 'estimated',
|
|
kind: 'estimated',
|
|
|
- tokens: estimateHeader(header) + state.surfaceTokens,
|
|
|
|
|
|
|
+ tokens: estimateHeader(header) + surface.surfaceTokens,
|
|
|
}
|
|
}
|
|
|
surfaceDeltaTokens = 0
|
|
surfaceDeltaTokens = 0
|
|
|
}
|
|
}
|
|
@@ -141,11 +167,18 @@ export class TokenMeter extends Service {
|
|
|
baseline,
|
|
baseline,
|
|
|
surfaceDeltaTokens,
|
|
surfaceDeltaTokens,
|
|
|
totalTokens: Math.max(0, baseline.tokens + surfaceDeltaTokens),
|
|
totalTokens: Math.max(0, baseline.tokens + surfaceDeltaTokens),
|
|
|
- surfaceTokens: state.surfaceTokens,
|
|
|
|
|
- nodes: state.surface,
|
|
|
|
|
|
|
+ surfaceTokens: surface.surfaceTokens,
|
|
|
|
|
+ nodes: surface.nodes,
|
|
|
}))
|
|
}))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /** Resolve the routed model's image pricing, when the llm service and route declare one. */
|
|
|
|
|
+ private _routeImagePricing(header: EpochHeader | undefined): LlmImageRequestPricing | undefined {
|
|
|
|
|
+ const config = header?.config
|
|
|
|
|
+ if (config === undefined) return undefined
|
|
|
|
|
+ return this.ctx.get('llm')?.imageRequestPricing(config.provider, config.model)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Heuristically price one model-visible message (instance face of the pure
|
|
* Heuristically price one model-visible message (instance face of the pure
|
|
|
* `estimateMessage` export from `estimate.ts`).
|
|
* `estimateMessage` export from `estimate.ts`).
|
|
@@ -164,7 +197,6 @@ export class TokenMeter extends Service {
|
|
|
consumedEvents: 0,
|
|
consumedEvents: 0,
|
|
|
header: undefined,
|
|
header: undefined,
|
|
|
surface: [],
|
|
surface: [],
|
|
|
- surfaceTokens: 0,
|
|
|
|
|
stepStart: undefined,
|
|
stepStart: undefined,
|
|
|
anchor: undefined,
|
|
anchor: undefined,
|
|
|
}
|
|
}
|
|
@@ -200,7 +232,7 @@ export class TokenMeter extends Service {
|
|
|
`token meter: step/start at seq ${event.seq} arrived before turn ${state.stepStart.turn}/step ${state.stepStart.step} ended`,
|
|
`token meter: step/start at seq ${event.seq} arrived before turn ${state.stepStart.turn}/step ${state.stepStart.step} ended`,
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
- nextStepStart = { ...event.data, surfaceTokens: state.surfaceTokens }
|
|
|
|
|
|
|
+ nextStepStart = { ...event.data, nodes: [...state.surface] }
|
|
|
break
|
|
break
|
|
|
case 'step/end':
|
|
case 'step/end':
|
|
|
if (state.stepStart === undefined
|
|
if (state.stepStart === undefined
|
|
@@ -230,32 +262,18 @@ export class TokenMeter extends Service {
|
|
|
// oxlint-disable-next-line typescript/no-non-null-assertion
|
|
// oxlint-disable-next-line typescript/no-non-null-assertion
|
|
|
const eventTokens = plan!.tokens
|
|
const eventTokens = plan!.tokens
|
|
|
if (event.data.usage !== undefined && nextHeader !== undefined) {
|
|
if (event.data.usage !== undefined && nextHeader !== undefined) {
|
|
|
- const providerAssistantTokens = this._estimateProviderAssistant(
|
|
|
|
|
- session,
|
|
|
|
|
- event,
|
|
|
|
|
- eventTokens,
|
|
|
|
|
- )
|
|
|
|
|
- const anchorSurfaceTokens = stepStart.surfaceTokens + providerAssistantTokens
|
|
|
|
|
- const providerTokens = usageTokens(event.data.usage)
|
|
|
|
|
- const estimatedAnchorTokens = estimateHeader(nextHeader) + anchorSurfaceTokens
|
|
|
|
|
nextAnchor = {
|
|
nextAnchor = {
|
|
|
header: nextHeader,
|
|
header: nextHeader,
|
|
|
- surfaceTokens: anchorSurfaceTokens,
|
|
|
|
|
- // Signed heuristic deltas remain conservative only from an anchor
|
|
|
|
|
- // that is at least as large as the matching full heuristic price.
|
|
|
|
|
- baseline: providerTokens >= estimatedAnchorTokens
|
|
|
|
|
- ? { kind: 'usage', tokens: providerTokens, usage: event.data.usage }
|
|
|
|
|
- : { kind: 'estimated', tokens: estimatedAnchorTokens },
|
|
|
|
|
|
|
+ nodes: stepStart.nodes,
|
|
|
|
|
+ assistantTokens: this._estimateProviderAssistant(session, event, eventTokens),
|
|
|
|
|
+ usage: event.data.usage,
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
- const anchorSurfaceTokens = stepStart.surfaceTokens + eventTokens
|
|
|
|
|
nextAnchor = {
|
|
nextAnchor = {
|
|
|
header: nextHeader,
|
|
header: nextHeader,
|
|
|
- surfaceTokens: anchorSurfaceTokens,
|
|
|
|
|
- baseline: {
|
|
|
|
|
- kind: 'estimated',
|
|
|
|
|
- tokens: estimateHeader(nextHeader) + anchorSurfaceTokens,
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ nodes: stepStart.nodes,
|
|
|
|
|
+ assistantTokens: eventTokens,
|
|
|
|
|
+ usage: undefined,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -264,7 +282,6 @@ export class TokenMeter extends Service {
|
|
|
state.stepStart = nextStepStart
|
|
state.stepStart = nextStepStart
|
|
|
if (plan !== undefined) {
|
|
if (plan !== undefined) {
|
|
|
commitSurfaceTokens(state.surface, plan)
|
|
commitSurfaceTokens(state.surface, plan)
|
|
|
- state.surfaceTokens += plan.deltaTokens
|
|
|
|
|
}
|
|
}
|
|
|
state.anchor = nextAnchor
|
|
state.anchor = nextAnchor
|
|
|
}
|
|
}
|