@vincemakes/kiso-runtime 0.29.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agent.d.ts CHANGED
@@ -35,10 +35,22 @@ export interface AgentDefinition {
35
35
  readonly hooks?: HookHost;
36
36
  /** Direct adapter injection (tests, faux, custom providers). */
37
37
  readonly adapter?: Adapter;
38
- /** Lazy provider: "anthropic" | "openai-compat" (imports the peer package). */
39
- readonly provider?: "anthropic" | "openai-compat";
38
+ /** Lazy provider: "anthropic" | "openai-compat" | "openai-responses"
39
+ * (imports the peer package). */
40
+ readonly provider?: "anthropic" | "openai-compat" | "openai-responses";
40
41
  readonly apiKey?: string;
41
42
  readonly baseUrl?: string;
43
+ /** OR-1: the Responses adapter's ChatGPT target — a thunk that yields
44
+ * a FRESH token per request (the CLI's calls the credential store's
45
+ * refresh). Its presence is what selects that target; `apiKey`
46
+ * selects the first-party one. Ignored by the other providers. */
47
+ readonly oauth?: () => Promise<{
48
+ readonly access: string;
49
+ readonly accountId: string;
50
+ }>;
51
+ /** OR-1: the ChatGPT backend's prefix-cache key — the session id, so
52
+ * one session's requests share a cache lane. */
53
+ readonly promptCacheKey?: string;
42
54
  /** PH-1c.1: opt-in Anthropic prompt caching (cache_control
43
55
  * breakpoints) — OFF by default; the openai-compat path ignores it
44
56
  * (that dialect's caching is server-automatic). Type-only additive. */
@@ -83,8 +95,13 @@ export declare function createAgent(definition: AgentDefinition): AgentRuntime;
83
95
  * adapter each call; the caller (session.setAdapter) decides when it
84
96
  * takes effect.
85
97
  */
86
- export declare function buildAdapter(provider: "anthropic" | "openai-compat", opts?: {
98
+ export declare function buildAdapter(provider: "anthropic" | "openai-compat" | "openai-responses", opts?: {
87
99
  readonly apiKey?: string;
88
100
  readonly baseUrl?: string;
89
101
  readonly promptCaching?: boolean;
102
+ readonly oauth?: () => Promise<{
103
+ readonly access: string;
104
+ readonly accountId: string;
105
+ }>;
106
+ readonly promptCacheKey?: string;
90
107
  }): Promise<Adapter>;
package/dist/agent.js CHANGED
@@ -121,6 +121,9 @@ export class AgentRuntime {
121
121
  const config = {
122
122
  model: restored?.model ?? this.#definition.model,
123
123
  ...(this.#definition.provider !== undefined ? { provider: this.#definition.provider } : {}),
124
+ // OR-1: the endpoint rides with the provider — the cost path and
125
+ // the window lookup key on (model, endpoint), never on the id alone.
126
+ ...(this.#definition.baseUrl !== undefined ? { baseUrl: this.#definition.baseUrl } : {}),
124
127
  ...((restored !== null ? restored.scope : startupScope) !== undefined
125
128
  ? { continuationScope: (restored !== null ? restored.scope : startupScope) }
126
129
  : {}),
@@ -217,7 +220,25 @@ async function resolveAdapter(definition) {
217
220
  : {}),
218
221
  });
219
222
  }
223
+ case "openai-responses": {
224
+ const { createOpenAIResponsesProvider } = await import("@vincemakes/kiso-provider-openai-responses");
225
+ // MG-1 (A5): the adapter's replay identity is the SAME one the
226
+ // run's stamping scope uses, so emit and replay agree — the
227
+ // openai-compat case's rule, one dialect over.
228
+ const scope = resolveContinuationScope("openai-responses", "", definition.baseUrl);
229
+ // The adapter picks its TARGET from these options — an `oauth`
230
+ // thunk is the ChatGPT backend, an `apiKey` is the first-party
231
+ // API — so no separate switch exists to disagree with the
232
+ // credential the CLI resolved.
233
+ return createOpenAIResponsesProvider({
234
+ ...(scope !== undefined ? { scope: { providerId: scope.providerId } } : {}),
235
+ ...(definition.apiKey !== undefined ? { apiKey: definition.apiKey } : {}),
236
+ ...(definition.oauth !== undefined ? { oauth: definition.oauth } : {}),
237
+ ...(definition.baseUrl !== undefined ? { baseUrl: definition.baseUrl } : {}),
238
+ ...(definition.promptCacheKey !== undefined ? { promptCacheKey: definition.promptCacheKey } : {}),
239
+ });
240
+ }
220
241
  default:
221
- throw new Error("createAgent: pass an `adapter` or a `provider` (\"anthropic\" | \"openai-compat\")");
242
+ throw new Error("createAgent: pass an `adapter` or a `provider` (\"anthropic\" | \"openai-compat\" | \"openai-responses\")");
222
243
  }
223
244
  }
@@ -25,11 +25,14 @@ export interface ProviderManifest {
25
25
  readonly apiIds: readonly string[];
26
26
  readonly defaultEndpoint?: string;
27
27
  }
28
- /** The five built-in identities. `custom` is the honest bucket for any
28
+ /** The six built-in identities. `custom` is the honest bucket for any
29
29
  * OpenAI-compatible endpoint the origin table does not recognize —
30
- * capabilities default to UNKNOWN there, never borrowed from OpenAI. */
30
+ * capabilities default to UNKNOWN there, never borrowed from OpenAI.
31
+ * `chatgpt` is a PROVIDER, not a dialect: the subscription backend
32
+ * serves the Responses dialect and nothing else, and it authenticates
33
+ * by OAuth only — there is no API key that reaches it. */
31
34
  export declare const BUILTIN_MANIFESTS: readonly ProviderManifest[];
32
35
  /** The run's continuation scope, resolved from the live binding. An
33
36
  * undefined provider (a directly injected SDK/faux adapter) is an
34
37
  * UNSCOPED run: the kernel strips adapter-emitted continuation. */
35
- export declare function resolveContinuationScope(provider: "anthropic" | "openai-compat" | undefined, model: string, baseUrl?: string): ContinuationScope | undefined;
38
+ export declare function resolveContinuationScope(provider: "anthropic" | "openai-compat" | "openai-responses" | undefined, model: string, baseUrl?: string): ContinuationScope | undefined;
@@ -11,12 +11,16 @@
11
11
  *
12
12
  * Lives under runtime/internal — the curated root surface does not move.
13
13
  */
14
- /** The five built-in identities. `custom` is the honest bucket for any
14
+ /** The six built-in identities. `custom` is the honest bucket for any
15
15
  * OpenAI-compatible endpoint the origin table does not recognize —
16
- * capabilities default to UNKNOWN there, never borrowed from OpenAI. */
16
+ * capabilities default to UNKNOWN there, never borrowed from OpenAI.
17
+ * `chatgpt` is a PROVIDER, not a dialect: the subscription backend
18
+ * serves the Responses dialect and nothing else, and it authenticates
19
+ * by OAuth only — there is no API key that reaches it. */
17
20
  export const BUILTIN_MANIFESTS = [
18
21
  { id: "anthropic", revision: "2026-08-26.1", authMethods: ["api-key", "none"], apiIds: ["anthropic-messages"], defaultEndpoint: "https://api.anthropic.com" },
19
22
  { id: "openai", revision: "2026-08-26.1", authMethods: ["api-key", "none"], apiIds: ["openai-chat", "openai-responses"], defaultEndpoint: "https://api.openai.com" },
23
+ { id: "chatgpt", revision: "2026-09-08.1", authMethods: ["oauth"], apiIds: ["openai-responses"], defaultEndpoint: "https://chatgpt.com/backend-api" },
20
24
  { id: "deepseek", revision: "2026-08-26.1", authMethods: ["api-key", "none"], apiIds: ["openai-chat"], defaultEndpoint: "https://api.deepseek.com" },
21
25
  { id: "zai", revision: "2026-08-26.1", authMethods: ["api-key", "none"], apiIds: ["openai-chat"], defaultEndpoint: "https://api.z.ai" },
22
26
  { id: "custom", revision: "2026-08-26.1", authMethods: ["api-key", "none"], apiIds: ["openai-chat"] },
@@ -31,6 +35,21 @@ const KNOWN_ORIGINS = {
31
35
  "https://open.bigmodel.cn": "zai",
32
36
  "https://api.openai.com": "openai",
33
37
  };
38
+ /** The ChatGPT backend's origin. Deliberately NOT in KNOWN_ORIGINS: that
39
+ * table resolves openai-CHAT profiles, and the subscription backend does
40
+ * not speak that dialect — a compat profile pointed at it is broken, not
41
+ * a `chatgpt` binding. */
42
+ const CHATGPT_ORIGIN = "https://chatgpt.com";
43
+ function isChatgptOrigin(baseUrl) {
44
+ if (baseUrl === undefined)
45
+ return false;
46
+ try {
47
+ return new URL(baseUrl).origin === CHATGPT_ORIGIN;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
34
53
  /** The run's continuation scope, resolved from the live binding. An
35
54
  * undefined provider (a directly injected SDK/faux adapter) is an
36
55
  * UNSCOPED run: the kernel strips adapter-emitted continuation. */
@@ -40,6 +59,14 @@ export function resolveContinuationScope(provider, model, baseUrl) {
40
59
  if (provider === "anthropic") {
41
60
  return { providerId: "anthropic", apiId: "anthropic-messages", modelId: model };
42
61
  }
62
+ if (provider === "openai-responses") {
63
+ // Two identities serve this dialect and no others, so there is no
64
+ // `custom` bucket here: the ChatGPT origin, or OpenAI. This is the
65
+ // ONLY place the rule lives — the adapter is HANDED the result
66
+ // (agent.ts's openai-responses case) rather than re-deriving it,
67
+ // so the stamping side and the replay side cannot disagree.
68
+ return { providerId: isChatgptOrigin(baseUrl) ? "chatgpt" : "openai", apiId: "openai-responses", modelId: model };
69
+ }
43
70
  if (baseUrl === undefined) {
44
71
  return { providerId: "openai", apiId: "openai-chat", modelId: model };
45
72
  }
@@ -42,7 +42,9 @@ export interface ModelCapabilities {
42
42
  * effort affects whole responses with or without explicit thinking, and
43
43
  * some model/effort combinations forbid thinking-disabled. */
44
44
  export type ThinkingMode = "default" | "adaptive" | "enabled" | "disabled";
45
- export type ReasoningEffort = "default" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
45
+ /** `none` is the OpenAI Responses dialect's zero-reasoning level — a NATIVE
46
+ * value a request carries, distinct from "default" (no field sent). */
47
+ export type ReasoningEffort = "default" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
46
48
  export interface ReasoningSetting {
47
49
  readonly thinking: ThinkingMode;
48
50
  readonly effort: ReasoningEffort;
@@ -97,6 +97,47 @@ function anthropicLine() {
97
97
  row("claude-haiku-4-5", haiku, haikuPricing),
98
98
  ];
99
99
  }
100
+ const OPENAI_MODELS_ASOF = "2026-09-08";
101
+ /** the vendor CLI's per-model presets, pinned (the levels it offers a
102
+ * subscription; a first-party model page says nothing about that backend). */
103
+ const CHATGPT_PRESETS_SOURCE = "https://github.com/openai/codex/blob/37f4bb94c9f4e180535a12e3f2c2f93f4a773df0/codex-rs/models-manager/models.json";
104
+ const RESPONSES_WIRE = "reasoning.effort";
105
+ /** A first-party Responses row: the model page is the source for the
106
+ * levels, the window, the output cap and the price alike (read the same
107
+ * day). The adapter asks for no reasoning summaries (encrypted items
108
+ * only), so no thinking stream is emitted. */
109
+ const openaiRow = (model, effortDefault, rate, page) => ({
110
+ model,
111
+ providerId: "openai",
112
+ endpoint: "https://api.openai.com",
113
+ capabilities: { contextWindow: 1_050_000, maxOutputTokens: 128_000, promptCaching: "automatic", reasoning: {
114
+ emitsThinkingStream: false,
115
+ thinking: null,
116
+ effort: { levels: ["none", "low", "medium", "high", "xhigh"], default: effortDefault, wire: RESPONSES_WIRE },
117
+ asOf: OPENAI_MODELS_ASOF,
118
+ source: page,
119
+ }, inputModalities: null },
120
+ capabilitiesAsOf: OPENAI_MODELS_ASOF,
121
+ capabilitiesSource: page,
122
+ pricing: { ...rate, cacheWritePerM: 0, asOf: OPENAI_MODELS_ASOF, source: page },
123
+ });
124
+ /** The subscription backend's row for the same id: the presets' four
125
+ * levels (no `none`), the presets' context window, no price. */
126
+ const chatgptRow = (model) => ({
127
+ model,
128
+ providerId: "chatgpt",
129
+ endpoint: "https://chatgpt.com",
130
+ capabilities: { contextWindow: 272_000, maxOutputTokens: null, promptCaching: null, reasoning: {
131
+ emitsThinkingStream: false,
132
+ thinking: null,
133
+ effort: { levels: ["low", "medium", "high", "xhigh"], default: "medium", wire: RESPONSES_WIRE },
134
+ asOf: OPENAI_MODELS_ASOF,
135
+ source: CHATGPT_PRESETS_SOURCE,
136
+ }, inputModalities: null },
137
+ capabilitiesAsOf: OPENAI_MODELS_ASOF,
138
+ capabilitiesSource: CHATGPT_PRESETS_SOURCE,
139
+ pricing: null,
140
+ });
100
141
  /** The v1 table. Nulls outnumber numbers ON PURPOSE: only values with a
101
142
  * named source enter; everything else waits for one. */
102
143
  const ENTRIES = [
@@ -174,6 +215,23 @@ const ENTRIES = [
174
215
  capabilities: { contextWindow: 128_000, maxOutputTokens: null, promptCaching: "automatic", reasoning: { emitsThinkingStream: false, thinking: null, effort: null, asOf: null, source: null }, inputModalities: null },
175
216
  pricing: null,
176
217
  },
218
+ // OR-1 (2026-09-08): the Responses-dialect rows. The SAME model id gets
219
+ // TWO rows because it answers differently at each endpoint: the
220
+ // first-party API documents `none` and a per-token price; the
221
+ // subscription backend is paid by the subscription (pricing: null — a
222
+ // first-party rate must never be shown as if it were the bill) and
223
+ // offers the levels the vendor's own CLI presets list, pinned to a
224
+ // commit. The first-party row is listed FIRST on purpose: an
225
+ // endpoint-less lookup (a caller with no baseUrl — a direct-write
226
+ // profile, the faux path) resolves to it, and its levels are a
227
+ // superset of the subscription row's. The run-side resolver passes
228
+ // the binding's endpoint (the second OR-1 review), so this ordering
229
+ // is the endpoint-less FALLBACK, not the guard that keeps a
230
+ // subscription run from carrying a level its backend lacks.
231
+ openaiRow("gpt-5.5", "medium", { inputPerM: 5, outputPerM: 30, cacheReadPerM: 0.5 }, "https://developers.openai.com/api/docs/models/gpt-5.5"),
232
+ openaiRow("gpt-5.4", "none", { inputPerM: 2.5, outputPerM: 15, cacheReadPerM: 0.25 }, "https://developers.openai.com/api/docs/models/gpt-5.4"),
233
+ chatgptRow("gpt-5.5"),
234
+ chatgptRow("gpt-5.4"),
177
235
  ];
178
236
  /**
179
237
  * Look a model up. `endpoint` narrows: an entry WITH an endpoint only
package/dist/run.js CHANGED
@@ -100,6 +100,8 @@ export class Run {
100
100
  runId: this.runId,
101
101
  provider: this.#config.provider ?? "adapter",
102
102
  model: this.#config.model,
103
+ // OR-1: the binding's endpoint — the record's price keys on it.
104
+ ...(this.#config.baseUrl !== undefined ? { endpoint: this.#config.baseUrl } : {}),
103
105
  adapterVersion: runtimeVersion(),
104
106
  log: log.all,
105
107
  rentParts,
@@ -135,7 +137,12 @@ export class Run {
135
137
  // XP-1: native-only resolution — an unsupported recorded
136
138
  // setting REFUSES the run with the reason (no silent
137
139
  // downgrade); default/default adds nothing (byte anchor).
138
- ...(this.#config.reasoning !== undefined ? resolveReasoningOrThrow(this.#config.model, this.#config.reasoning) : {}),
140
+ // OR-1: resolved against the binding's ENDPOINT — the same
141
+ // model id offers different levels at the first-party API
142
+ // and at the subscription backend, and the CLI's /model
143
+ // pre-check is not the runtime's guarantee (a direct
144
+ // setModelBinding or a recorded profile reaches here without it).
145
+ ...(this.#config.reasoning !== undefined ? resolveReasoningOrThrow(this.#config.model, this.#config.reasoning, this.#config.baseUrl) : {}),
139
146
  // E1: the composed approval chain — the extensions'
140
147
  // policies composed into ONE gate (deny > allow > ask).
141
148
  ...(approvalChain !== undefined ? { approvalPolicy: approvalChain } : {}),
@@ -787,8 +794,8 @@ export class Run {
787
794
  }
788
795
  /** XP-1: the run-side half of "no silent downgrade" — a setting the
789
796
  * matrix refuses becomes an actionable failure, never a quiet default. */
790
- function resolveReasoningOrThrow(model, setting) {
791
- const r = resolveReasoning(model, setting);
797
+ function resolveReasoningOrThrow(model, setting, endpoint) {
798
+ const r = resolveReasoning(model, setting, endpoint);
792
799
  if (!r.ok)
793
800
  throw new Error(`the session's recorded reasoning setting cannot run here: ${r.reason}`);
794
801
  return Object.keys(r.wire).length > 0 ? { reasoning: r.wire } : {};
package/dist/session.d.ts CHANGED
@@ -130,7 +130,11 @@ export declare class AgentSession {
130
130
  setModelBinding(binding: {
131
131
  readonly adapter: Adapter;
132
132
  readonly model: string;
133
- readonly provider?: "anthropic" | "openai-compat";
133
+ readonly provider?: "anthropic" | "openai-compat" | "openai-responses";
134
+ /** OR-1: the endpoint moves with the adapter too — the cost path
135
+ * and the window lookup key on (model, endpoint); absent = an
136
+ * endpoint-less lookup (the first row for the id). */
137
+ readonly baseUrl?: string;
134
138
  /** MG-1 (A5): the run's continuation scope — moves atomically with
135
139
  * the adapter (absent = unscoped: the kernel strips envelopes). */
136
140
  readonly scope?: ContinuationScope;
@@ -138,11 +142,15 @@ export declare class AgentSession {
138
142
  * fresh defaults (a new binding never inherits stale effort). */
139
143
  readonly reasoning?: ReasoningSetting;
140
144
  }): void;
141
- /** E2: the adapter identity ("anthropic" | "openai-compat") — the route
145
+ /** E2: the adapter identity (anthropic / openai-compat / openai-responses) — the route
142
146
  * key the canonical consumer (CLI usage, the trace block) keys on. The
143
147
  * per-run tracer reads the SAME live binding; one source, one
144
148
  * route — the CLI and the trace can never disagree. */
145
- get provider(): "anthropic" | "openai-compat" | undefined;
149
+ get provider(): "anthropic" | "openai-compat" | "openai-responses" | undefined;
150
+ /** OR-1: the live binding's endpoint — what the CLI hands the cost
151
+ * path next to `provider` and `model`, so the status row's dollar
152
+ * figure and the ledger's can never key on different registry rows. */
153
+ get baseUrl(): string | undefined;
146
154
  /** XP-1: the model that will answer the NEXT request — the live
147
155
  * binding, restored from the durable profile on open. The status row
148
156
  * reads THIS, so the row and the request can never disagree. */
@@ -376,9 +384,15 @@ export interface ContextPolicy {
376
384
  }
377
385
  export interface SessionConfig {
378
386
  readonly model: string;
379
- /** E1: the adapter identity ("anthropic" | "openai-compat") — trace
387
+ /** E1: the adapter identity (anthropic / openai-compat / openai-responses) — trace
380
388
  * provenance, additive (S1 surface untouched: type-only, optional). */
381
- readonly provider?: "anthropic" | "openai-compat";
389
+ readonly provider?: "anthropic" | "openai-compat" | "openai-responses";
390
+ /** OR-1: the adapter's endpoint. The registry row for one model id can
391
+ * differ per endpoint (gpt-5.5 is priced at the first-party API and
392
+ * unpriced at the ChatGPT backend), so the cost path and the window
393
+ * lookup key on (model, endpoint). Travels with the live binding,
394
+ * exactly as `provider` does. Type-only, optional. */
395
+ readonly baseUrl?: string;
382
396
  /** MG-1 (A5): the run's continuation scope — the kernel stamps it on
383
397
  * committed envelopes; absent = unscoped (envelopes stripped). */
384
398
  readonly continuationScope?: import("@vincemakes/kiso-core").ContinuationScope;
package/dist/session.js CHANGED
@@ -85,6 +85,8 @@ export class AgentSession {
85
85
  // setAdapter always had).
86
86
  #model;
87
87
  #provider;
88
+ // OR-1: the fourth passenger — the endpoint moves with the adapter too.
89
+ #baseUrl;
88
90
  // MG-1 (A5): travels WITH the adapter, same next-turn semantics.
89
91
  #continuationScope;
90
92
  // XP-1: the selected axes; resolved per request (next-turn semantics).
@@ -140,6 +142,7 @@ export class AgentSession {
140
142
  this.#config = composedHooks === undefined ? config : { ...config, hooks: composedHooks };
141
143
  this.#model = config.model;
142
144
  this.#provider = config.provider;
145
+ this.#baseUrl = config.baseUrl;
143
146
  this.#continuationScope = config.continuationScope;
144
147
  this.#reasoning = config.reasoning ?? { thinking: "default", effort: "default" };
145
148
  this.#profilePending = config.profilePending === true;
@@ -149,11 +152,12 @@ export class AgentSession {
149
152
  * fresh per call so an in-flight run keeps the config it started with
150
153
  * — the same boundary setAdapter has always drawn. */
151
154
  #effectiveConfig() {
152
- const { provider: _startup, continuationScope: _startupScope, ...rest } = this.#config;
155
+ const { provider: _startup, baseUrl: _startupUrl, continuationScope: _startupScope, ...rest } = this.#config;
153
156
  return {
154
157
  ...rest,
155
158
  model: this.#model,
156
159
  ...(this.#provider !== undefined ? { provider: this.#provider } : {}),
160
+ ...(this.#baseUrl !== undefined ? { baseUrl: this.#baseUrl } : {}),
157
161
  ...(this.#continuationScope !== undefined ? { continuationScope: this.#continuationScope } : {}),
158
162
  reasoning: this.#reasoning,
159
163
  };
@@ -216,19 +220,26 @@ export class AgentSession {
216
220
  this.#adapter = binding.adapter;
217
221
  this.#model = binding.model;
218
222
  this.#provider = binding.provider;
223
+ this.#baseUrl = binding.baseUrl;
219
224
  this.#continuationScope = binding.scope;
220
225
  this.#reasoning = binding.reasoning ?? { thinking: "default", effort: "default" };
221
226
  // XP-1: an explicit selection is DURABLE — the setting survives
222
227
  // /resume because a revision records it now, not at some later flush.
223
228
  this.#recordProfile();
224
229
  }
225
- /** E2: the adapter identity ("anthropic" | "openai-compat") — the route
230
+ /** E2: the adapter identity (anthropic / openai-compat / openai-responses) — the route
226
231
  * key the canonical consumer (CLI usage, the trace block) keys on. The
227
232
  * per-run tracer reads the SAME live binding; one source, one
228
233
  * route — the CLI and the trace can never disagree. */
229
234
  get provider() {
230
235
  return this.#provider;
231
236
  }
237
+ /** OR-1: the live binding's endpoint — what the CLI hands the cost
238
+ * path next to `provider` and `model`, so the status row's dollar
239
+ * figure and the ledger's can never key on different registry rows. */
240
+ get baseUrl() {
241
+ return this.#baseUrl;
242
+ }
232
243
  /** XP-1: the model that will answer the NEXT request — the live
233
244
  * binding, restored from the durable profile on open. The status row
234
245
  * reads THIS, so the row and the request can never disagree. */
@@ -314,6 +325,8 @@ export class AgentSession {
314
325
  runId,
315
326
  provider: this.#provider ?? "adapter",
316
327
  model: this.#model,
328
+ // OR-1: the side query is priced by the same live endpoint as a run.
329
+ ...(this.#baseUrl !== undefined ? { endpoint: this.#baseUrl } : {}),
317
330
  adapterVersion: runtimeVersion(),
318
331
  purpose: options.purpose,
319
332
  // the manifest's seqRange pointers derive from the log, and a side
@@ -418,6 +431,14 @@ export class AgentSession {
418
431
  });
419
432
  let summary;
420
433
  let usage = null;
434
+ // OR-1 (the second review): the summary is priced by the binding that
435
+ // MADE the request. A /model switch during this call — the session's
436
+ // longest single request — moves #model/#baseUrl/#provider under it;
437
+ // reading them after the await priced a subscription summary at the
438
+ // first-party rate. ONE snapshot serves the request and the ledger
439
+ // line, the PH-F8 law (adapter, model, provider move together)
440
+ // applied to the one off-loop call.
441
+ const binding = { adapter: this.#adapter, model: this.#model, baseUrl: this.#baseUrl, provider: this.#provider };
421
442
  if (options.drop === true) {
422
443
  // E6 — the crux drop arm: mechanical, no model call, the fixed
423
444
  // placeholder replaces the covered range (experiment-only).
@@ -425,8 +446,8 @@ export class AgentSession {
425
446
  }
426
447
  else {
427
448
  const call = await summarizeConversation({
428
- adapter: this.#adapter,
429
- model: this.#model,
449
+ adapter: binding.adapter,
450
+ model: binding.model,
430
451
  // E6 (a): ONE serialized user message — the DSML bug's
431
452
  // raw-message array is structurally dead on this path.
432
453
  messages: [{ role: "user", content: serializedInput }],
@@ -460,7 +481,7 @@ export class AgentSession {
460
481
  // a degraded ledger costs one stderr line, never the summary.
461
482
  if (usage !== null) {
462
483
  try {
463
- const canonical = canonicalizeUsageForModel(this.#model, undefined, this.#provider ?? "adapter", usage);
484
+ const canonical = canonicalizeUsageForModel(binding.model, binding.baseUrl, binding.provider ?? "adapter", usage);
464
485
  const line = JSON.stringify({ kind: "summary", canonical }) + "\n";
465
486
  mkdirSync(join(this.#store.root, "traces"), { recursive: true });
466
487
  appendFileSync(join(this.#store.root, "traces", `${this.id}.jsonl`), line);
@@ -29,6 +29,9 @@ export interface RequestTracerDeps {
29
29
  runId: string;
30
30
  provider: string;
31
31
  model: string;
32
+ /** OR-1: the adapter's endpoint — the registry row (and so the price)
33
+ * for one model id can differ per endpoint; absent = endpoint-less. */
34
+ endpoint?: string;
32
35
  /** The adapter contract's implementation version (the runtime's own),
33
36
  * resolved once at tracer init; null on failure (soft-fail). */
34
37
  adapterVersion?: string | null;
@@ -33,6 +33,7 @@ export class RequestTracer {
33
33
  #writer;
34
34
  #log;
35
35
  #provider;
36
+ #endpoint;
36
37
  #runId;
37
38
  #adapterVersion;
38
39
  #rentParts;
@@ -43,6 +44,7 @@ export class RequestTracer {
43
44
  this.#writer = new TraceWriter({ root: deps.root, sessionId: deps.sessionId });
44
45
  this.#log = deps.log;
45
46
  this.#provider = deps.provider;
47
+ this.#endpoint = deps.endpoint;
46
48
  this.#runId = deps.runId;
47
49
  this.#adapterVersion = deps.adapterVersion ?? null;
48
50
  this.#rentParts = deps.rentParts;
@@ -208,7 +210,10 @@ export class RequestTracer {
208
210
  // MODEL via the metadata registry (route-keyed pricing priced an
209
211
  // Anthropic run at DeepSeek's rates); the convention still keys on
210
212
  // the route. An unpriced model costs null — never a fallback rate.
211
- record.canonical = canonicalizeUsageForModel(record.model, undefined, this.#provider, {
213
+ // OR-1: and on the binding's ENDPOINT — the same id is priced at the
214
+ // first-party API and unpriced at the subscription backend; an
215
+ // endpoint-less lookup here priced a subscription run at $5/$30.
216
+ record.canonical = canonicalizeUsageForModel(record.model, this.#endpoint, this.#provider, {
212
217
  inputTokens: p.inputTokens,
213
218
  outputTokens: p.outputTokens,
214
219
  cacheRead: p.cacheRead,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-runtime",
3
- "version": "0.29.0",
3
+ "version": "0.31.0",
4
4
  "description": "kiso runtime — durable multi-turn agent sessions: AgentDefinition, AgentRuntime, AgentSession, Run, append-only JSONL store.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,11 +25,12 @@
25
25
  "test": "vitest run"
26
26
  },
27
27
  "dependencies": {
28
- "@vincemakes/kiso-core": "0.29.0"
28
+ "@vincemakes/kiso-core": "0.31.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "@vincemakes/kiso-provider-anthropic": "0.29.0",
32
- "@vincemakes/kiso-provider-openai": "0.29.0"
31
+ "@vincemakes/kiso-provider-anthropic": "0.31.0",
32
+ "@vincemakes/kiso-provider-openai": "0.31.0",
33
+ "@vincemakes/kiso-provider-openai-responses": "0.31.0"
33
34
  },
34
35
  "peerDependenciesMeta": {
35
36
  "@vincemakes/kiso-provider-anthropic": {
@@ -37,10 +38,13 @@
37
38
  },
38
39
  "@vincemakes/kiso-provider-openai": {
39
40
  "optional": true
41
+ },
42
+ "@vincemakes/kiso-provider-openai-responses": {
43
+ "optional": true
40
44
  }
41
45
  },
42
46
  "devDependencies": {
43
- "@vincemakes/kiso-evals": "0.29.0",
47
+ "@vincemakes/kiso-evals": "0.31.0",
44
48
  "@types/node": "^26.1.2",
45
49
  "typescript": "^5.7.2",
46
50
  "vitest": "^3.0.0"