@ultimat3/ai 24.0.0 → 25.1.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/src/tools.ts CHANGED
@@ -6,8 +6,9 @@
6
6
  // permissions" concept in Ultimate, because there is no second authz system.
7
7
  //
8
8
  // `run` below is `ProjectableAction`'s — the projection SEAM, which is what carries `invoke`.
9
- // It is not a member of the action facade: an `action()` is `as`/`tool`/`openapi`/`job`/
10
- // `contract` and the callable itself, and this header claimed `action.run` until 2026-08.
9
+ // It is not a member of the action facade: an `action()` is `as`/`openapi`/`client`/`job`/
10
+ // `contract` and the callable itself (no `tool` since 25.0.0 — `@ultimat3/mcp`'s `toolFrom` is
11
+ // the MCP projection), and this header claimed `action.run` until 2026-08.
11
12
  // `asProjectableAction` is what BUILDS that seam out of a real `action()`, so an app writes
12
13
  // `agent({ tools: [publishPost] })` and never a hand-shaped stand-in — the same union
13
14
  // @ultimat3/mcp's `ListedPrimitive` accepts, adapted at this package's own edge because the two
@@ -20,14 +21,15 @@
20
21
  import type { AnyAction } from '@ultimat3/action';
21
22
  import { actionName, invoke, isAction } from '@ultimat3/action';
22
23
  import type { Actor } from '@ultimat3/core';
23
- import { hasPublicCause, isMcpExposed, isUltimateError, stringField } from '@ultimat3/core';
24
- import { statusFor } from '@ultimat3/http';
24
+ import { isMcpExposed, tryUseContext } from '@ultimat3/core';
25
25
  import type { WireJsonSchema } from '@ultimat3/schema';
26
26
  import { toWireSchema } from '@ultimat3/schema';
27
+ import { discloseFailure } from './failure-disclosure';
28
+ import { takeToolIdempotencyKey, withToolIdempotencyKey } from './tool-idempotency';
27
29
 
28
30
  /**
29
31
  * The JSON Schema subset the framework emits for tool arguments — `@ultimat3/schema`'s
30
- * `WireJsonSchema`, the one document `.tool()`, `tools/list` and the model are all handed.
32
+ * `WireJsonSchema`, the one document `toolFrom`, `tools/list` and the model are all handed.
31
33
  */
32
34
  export type JsonSchema = WireJsonSchema;
33
35
 
@@ -75,7 +77,7 @@ export interface ProjectableAction {
75
77
  *
76
78
  * The real `action()` comes first because it is what an app has. Until 2026-08 this list took
77
79
  * `ProjectableAction` alone, which no `action()` structurally satisfies — an action carries
78
- * `as`/`tool`/`openapi`/`job`/`contract` and never `run` — so the documented shape
80
+ * `as`/`openapi`/`client`/`job`/`contract` and never `run` — so the documented shape
79
81
  * `agent({ tools: [publishPost] })` was a `TS2741` and every test in this package hand-built a
80
82
  * stand-in, which is why the suite stayed green over an API that did not compile (issue #124).
81
83
  * `ProjectableAction` stays in the union for a surface that builds its catalog programmatically
@@ -84,8 +86,8 @@ export interface ProjectableAction {
84
86
  export type AgentTool = AnyAction | ProjectableAction;
85
87
 
86
88
  /**
87
- * Adapt whatever the author listed. The same shape @ultimat3/mcp's `asProjectable` produces, from
88
- * the same `invoke` — an in-app agent and an external MCP client end at one execution path, so one
89
+ * Adapt whatever the author listed. The same shape @ultimat3/mcp's `primitiveFromAction` produces,
90
+ * from the same `invoke` — an in-app agent and an external MCP client end at one execution path, so one
89
91
  * policy decides both. The execution adapter is not shared code and cannot be — `mcp` is this
90
92
  * package's own tier — but the schema is: both publish `@ultimat3/schema`'s `toWireSchema`.
91
93
  *
@@ -96,17 +98,30 @@ export type AgentTool = AnyAction | ProjectableAction;
96
98
  export function asProjectableAction(listed: AgentTool): ProjectableAction {
97
99
  if (!isAction(listed)) return listed;
98
100
  const mcp = listed.mcp;
101
+ // Throws `X_ACTION_UNREGISTERED` on an unnamed action rather than offering a tool called `''`:
102
+ // a nameless tool is unaddressable by the model, by `runLlmToolCall` and by the author.
103
+ const name = actionName(listed);
104
+ const wire = toWireSchema(listed.input);
105
+ // `idempotent: true` is honoured here as over MCP and HTTP: the model's key reaches `invoke`,
106
+ // which files it under the action, the caller and the key. Until 25.0.0 this adapter dropped it,
107
+ // so an action an external agent could retry safely was one an in-app agent could not.
108
+ const keyed = listed.describe().idempotent;
99
109
  return {
100
- // Throws `X_ACTION_UNREGISTERED` on an unnamed action rather than offering a tool called `''`:
101
- // a nameless tool is unaddressable by the model, by `runLlmToolCall` and by the author.
102
- name: actionName(listed),
110
+ name,
103
111
  ...(mcp === undefined ? {} : { mcp }),
104
112
  ...(mcp?.description === undefined ? {} : { description: mcp.description }),
105
- inputJsonSchema: toWireSchema(listed.input),
113
+ inputJsonSchema: keyed ? withToolIdempotencyKey(name, wire) : wire,
106
114
  // The actor rides in on the options and `invoke` swaps it inside the one execution path —
107
115
  // the action's own `policy` still decides, and its `input:` still parses what the model sent,
108
116
  // which is what drops a `{ actor: 'admin' }` the model invented before any handler sees it.
109
- run: ({ input, actor }) => invoke(listed, input, { surface: 'mcp', actor }),
117
+ // No `admit`: MCP's is its early gate ahead of ITS argument validation, and this surface has
118
+ // none — `invoke` asks the same policy before it reads the input, which is the same order.
119
+ run: keyed
120
+ ? ({ input: args, actor }) => {
121
+ const { input, idempotencyKey } = takeToolIdempotencyKey(args);
122
+ return invoke(listed, input, { surface: 'mcp', actor, idempotencyKey });
123
+ }
124
+ : ({ input, actor }) => invoke(listed, input, { surface: 'mcp', actor }),
110
125
  };
111
126
  }
112
127
 
@@ -162,7 +177,7 @@ export async function runLlmToolCall(
162
177
  return resultOf(call.id, output);
163
178
  } catch (error) {
164
179
  // A policy denial is an outcome the model should read and react to, not a crash.
165
- return { toolUseId: call.id, content: describeFailure(error), isError: true };
180
+ return { toolUseId: call.id, content: describeFailure(error, call.name), isError: true };
166
181
  }
167
182
  }
168
183
 
@@ -195,29 +210,16 @@ function resultOf(toolUseId: string, output: unknown): LlmToolResult {
195
210
  * it: the provider keeps every request it is sent, and a cause that reached it cannot be recalled.
196
211
  */
197
212
  export const HIDDEN_TOOL_CAUSE =
198
- 'the tool failed inside the server; the details are withheld from this conversation and are in the server logs';
213
+ 'the tool failed inside the server; the details are withheld from this conversation; the server log records its code and where it failed';
199
214
 
200
215
  /**
201
- * The thrown value, read STRUCTURALLY and totally. `stringField` from `@ultimat3/core`, never
202
- * `typeof e.code === 'string'`: the value is whatever an app's handler, its driver or its SDK
203
- * threw, so each read is a getter call or a `Proxy` trap — and it runs inside the catch block
204
- * that has nothing left to answer the model with if the probe itself raises.
205
- *
206
- * A 5xx code's cause is shown only when core's `hasPublicCause` allows it — the rule a production
207
- * problem document follows (`@ultimat3/http`'s `toProblem`). Hidden, the developer's `fix` goes with
208
- * it (a driver writes it from the statement it failed on); a `callerFix` an `UltimateError` built in
209
- * this process is authored for a remote reader and stays.
216
+ * `discloseFailure`'s verdict, rendered as the `tool_result` line — the same rule a hive member's
217
+ * reason follows, so what an agent may read of a throw is decided once. An uncoded throw says only
218
+ * that it failed; a withheld cause becomes `HIDDEN_TOOL_CAUSE`, and the log has the rest.
210
219
  */
211
- function describeFailure(error: unknown): string {
212
- const code = stringField(error, 'code');
213
- if (code === undefined) return 'tool failed';
214
- if (statusFor(code) >= 500 && !hasPublicCause(code)) {
215
- const callerFix = isUltimateError(error) ? stringField(error, 'callerFix') : undefined;
216
- return callerFix === undefined
217
- ? `${code}: ${HIDDEN_TOOL_CAUSE}`
218
- : `${code}: ${HIDDEN_TOOL_CAUSE} (fix: ${callerFix})`;
219
- }
220
- const cause = stringField(error, 'cause') ?? 'unknown';
221
- const fix = stringField(error, 'fix') ?? '';
222
- return fix === '' ? `${code}: ${cause}` : `${code}: ${cause} (fix: ${fix})`;
220
+ function describeFailure(error: unknown, toolName: string): string {
221
+ const shown = discloseFailure(error, tryUseContext()?.logger, `tool ${toolName}`);
222
+ if (shown.code === undefined) return 'tool failed';
223
+ const line = `${shown.code}: ${shown.cause ?? HIDDEN_TOOL_CAUSE}`;
224
+ return shown.fix === undefined ? line : `${line} (fix: ${shown.fix})`;
223
225
  }
@@ -33,10 +33,10 @@ export const UNSCOPED: VectorScope = Object.freeze({ crossTenant: true });
33
33
  export const UNBOUND: VectorScope = Object.freeze({});
34
34
 
35
35
  /** The tenant column value a row carries when its store had no tenant bound. */
36
- export const NO_TENANT = '';
36
+ export const NO_VECTOR_TENANT = '';
37
37
 
38
38
  export function tenantOf(scope: VectorScope): string {
39
- return scope.tenant ?? NO_TENANT;
39
+ return scope.tenant ?? NO_VECTOR_TENANT;
40
40
  }
41
41
 
42
42
  /**
package/src/vector.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // Vector storage and retrieval: the `VectorStore` contract and the in-memory dev store.
2
- // `PgVectorStore` — the production path — implements this same contract in `pg-vector.ts`.
2
+ // `PostgresVectorStore` — the production path — implements this same contract in `pg-vector.ts`.
3
3
  //
4
4
  // The store interface is shaped for pgvector (one table, one index, SQL you can read), with
5
5
  // an in-memory cosine implementation as the dev default. Hybrid search is first-class rather
@@ -9,11 +9,11 @@
9
9
  // be comparable — which they never are.
10
10
 
11
11
  import { finiteCount, finiteOption } from '@ultimat3/core';
12
- import { cosine, tokenize } from './embeddings';
12
+ import { cosine, wordTokens } from './embeddings';
13
13
  import { VectorDimMismatchError } from './errors';
14
14
  import {
15
15
  assertTenantRead,
16
- NO_TENANT,
16
+ NO_VECTOR_TENANT,
17
17
  narrowScope,
18
18
  scopeAdmits,
19
19
  UNBOUND,
@@ -95,6 +95,15 @@ export interface StoredRecord extends VectorRecord {
95
95
  readonly tenant: string;
96
96
  }
97
97
 
98
+ /** The row's primary key, `(tenant, id)` as the pg table declares it. NUL never occurs in a tenant. */
99
+ const storageKey = (tenant: string, id: string): string => `${tenant}\u0000${id}`;
100
+
101
+ /** A row and its score inside one ranking, before the tenant is dropped from the public hit. */
102
+ interface Scored {
103
+ readonly record: StoredRecord;
104
+ readonly score: number;
105
+ }
106
+
98
107
  export class MemoryVectorStore implements VectorStore {
99
108
  readonly name: string;
100
109
  readonly dimension: number;
@@ -119,7 +128,7 @@ export class MemoryVectorStore implements VectorStore {
119
128
  }
120
129
 
121
130
  /**
122
- * Same envelope, same rule as `PgVectorStore.scoped`. The dev store enforces it too, because
131
+ * Same envelope, same rule as `PostgresVectorStore.scoped`. The dev store enforces it too, because
123
132
  * a tenant leak that only reproduces against production Postgres is a leak nobody finds.
124
133
  */
125
134
  scoped(scope: VectorScope): MemoryVectorStore {
@@ -131,10 +140,10 @@ export class MemoryVectorStore implements VectorStore {
131
140
  }
132
141
 
133
142
  async upsert(records: readonly VectorRecord[]): Promise<void> {
134
- const tenant = this.scope.tenant ?? NO_TENANT;
143
+ const tenant = this.scope.tenant ?? NO_VECTOR_TENANT;
135
144
  for (const record of records) {
136
145
  this.assertDimension(record.vector.length);
137
- this.records.set(`${tenant}\u0000${record.id}`, { ...record, tenant });
146
+ this.records.set(storageKey(tenant, record.id), { ...record, tenant });
138
147
  }
139
148
  }
140
149
 
@@ -147,10 +156,9 @@ export class MemoryVectorStore implements VectorStore {
147
156
  this.assertDimension(vector.length);
148
157
  // `k` carries no default, so `??` never sees it and nothing else does either: `slice(0, NaN)`
149
158
  // is `[]`, which is a search that answers "no matches" for every query and reports success.
150
- return this.candidates(filter)
151
- .map((record) => this.hit(record, cosine(vector, record.vector)))
152
- .sort(byScoreDesc)
153
- .slice(0, finiteCount(SUBJECT, 'k', k));
159
+ return this.dense(vector, finiteCount(SUBJECT, 'k', k), filter).map(({ record, score }) =>
160
+ this.hit(record, score),
161
+ );
154
162
  }
155
163
 
156
164
  /** BM25 over the stored text. Real lexical scoring, so a rare exact term actually wins. */
@@ -160,12 +168,66 @@ export class MemoryVectorStore implements VectorStore {
160
168
  filter?: MetadataFilter,
161
169
  ): Promise<readonly SearchHit[]> {
162
170
  assertTenantRead(this.name, this.scope);
163
- const width = finiteCount(SUBJECT, 'k', k);
171
+ return this.lexical(query, finiteCount(SUBJECT, 'k', k), filter).map(({ record, score }) =>
172
+ this.hit(record, score),
173
+ );
174
+ }
175
+
176
+ /**
177
+ * Reciprocal-rank fusion. Each ranking contributes `1 / (rrfK + rank)`, so only ORDER
178
+ * matters — the two score scales never have to be reconciled, and a document ranked first
179
+ * by exact term match beats one that merely leads a flat vector ranking.
180
+ *
181
+ * Fused on the stored `(tenant, id)`, as `hybridSql` groups: an unscoped read sees every
182
+ * tenant's rows, and two tenants may share an id. Fusing on the id alone summed both rows'
183
+ * ranks into whichever came first and dropped the other. `fuse` keys on `SearchHit.id`, so the
184
+ * rankings reach it under the row's storage key and leave under the caller's id again.
185
+ */
186
+ async hybrid(input: HybridSearchInput): Promise<readonly SearchHit[]> {
187
+ assertTenantRead(this.name, this.scope);
188
+ this.assertDimension(input.vector.length);
189
+ // Three bounds, none of which `Math.max` screens — it PROPAGATES a `NaN`. A `NaN` `k` collapses
190
+ // both candidate widths and the final slice to `[]`; a `NaN` `rrfK` makes every fused score
191
+ // `NaN`, and the ranking this method exists to produce becomes whatever order the sort left.
192
+ const k = finiteCount(HYBRID, 'k', input.k);
193
+ const width = finiteCount(HYBRID, 'candidates', input.candidates ?? Math.max(k * 4, 20));
194
+ const rrfK = finiteOption(HYBRID, 'rrfK', input.rrfK ?? 60);
195
+ const rows = new Map<string, StoredRecord>();
196
+ const keyed = (ranking: readonly Scored[]): readonly SearchHit[] =>
197
+ ranking.map(({ record, score }) => {
198
+ const key = storageKey(record.tenant, record.id);
199
+ rows.set(key, record);
200
+ return { ...this.hit(record, score), id: key };
201
+ });
202
+ const fused = fuse(
203
+ [
204
+ keyed(this.dense(input.vector, width, input.filter)),
205
+ keyed(this.lexical(input.query, width, input.filter)),
206
+ ],
207
+ rrfK,
208
+ );
209
+ // Re-sorted once the ids are the caller's again: the tie-break is `d."id" asc`, never the key.
210
+ return fused
211
+ .map((hit) => ({ ...hit, id: rows.get(hit.id)?.id ?? hit.id }))
212
+ .sort(byScoreDesc)
213
+ .slice(0, k);
214
+ }
215
+
216
+ /** The cosine ranking `search` answers and `hybrid` fuses, rows still carrying their tenant. */
217
+ private dense(vector: Float32Array, k: number, filter?: MetadataFilter): readonly Scored[] {
218
+ return this.candidates(filter)
219
+ .map((record) => ({ record, score: cosine(vector, record.vector) }))
220
+ .sort(byRankDesc)
221
+ .slice(0, k);
222
+ }
223
+
224
+ /** BM25 over the stored text, rows still carrying their tenant. Only a positive score matches. */
225
+ private lexical(query: string, width: number, filter?: MetadataFilter): readonly Scored[] {
164
226
  const candidates = this.candidates(filter);
165
227
  if (candidates.length === 0) return [];
166
- const docs = candidates.map((record) => ({ record, tokens: tokenize(record.text) }));
228
+ const docs = candidates.map((record) => ({ record, tokens: wordTokens(record.text) }));
167
229
  const avgLength = docs.reduce((sum, d) => sum + d.tokens.length, 0) / docs.length;
168
- const terms = [...new Set(tokenize(query))];
230
+ const terms = [...new Set(wordTokens(query))];
169
231
 
170
232
  return docs
171
233
  .map(({ record, tokens }) => {
@@ -178,33 +240,13 @@ export class MemoryVectorStore implements VectorStore {
178
240
  const norm = this.k1 * (1 - this.b + (this.b * tokens.length) / avgLength);
179
241
  score += idf * ((tf * (this.k1 + 1)) / (tf + norm));
180
242
  }
181
- return this.hit(record, score);
243
+ return { record, score };
182
244
  })
183
- .filter((hit) => hit.score > 0)
184
- .sort(byScoreDesc)
245
+ .filter((scored) => scored.score > 0)
246
+ .sort(byRankDesc)
185
247
  .slice(0, width);
186
248
  }
187
249
 
188
- /**
189
- * Reciprocal-rank fusion. Each ranking contributes `1 / (rrfK + rank)`, so only ORDER
190
- * matters — the two score scales never have to be reconciled, and a document ranked first
191
- * by exact term match beats one that merely leads a flat vector ranking.
192
- */
193
- async hybrid(input: HybridSearchInput): Promise<readonly SearchHit[]> {
194
- assertTenantRead(this.name, this.scope);
195
- // Three bounds, none of which `Math.max` screens — it PROPAGATES a `NaN`. A `NaN` `k` collapses
196
- // both candidate widths and the final slice to `[]`; a `NaN` `rrfK` makes every fused score
197
- // `NaN`, and the ranking this method exists to produce becomes whatever order the sort left.
198
- const k = finiteCount(HYBRID, 'k', input.k);
199
- const width = finiteCount(HYBRID, 'candidates', input.candidates ?? Math.max(k * 4, 20));
200
- const rrfK = finiteOption(HYBRID, 'rrfK', input.rrfK ?? 60);
201
- const [dense, lexical] = await Promise.all([
202
- this.search(input.vector, width, input.filter),
203
- this.searchText(input.query, width, input.filter),
204
- ]);
205
- return fuse([dense, lexical], rrfK).slice(0, k);
206
- }
207
-
208
250
  /** Scoped and filtered, exactly like `pruneSql`: everything `filter` matches but `keep`. */
209
251
  async prune(filter: MetadataFilter, keep: readonly string[]): Promise<void> {
210
252
  const kept = new Set(keep);
@@ -247,6 +289,14 @@ export class MemoryVectorStore implements VectorStore {
247
289
  }
248
290
  }
249
291
 
292
+ /**
293
+ * The one way to build the in-process store — the twin of `postgresVectorStore()`. The class is a
294
+ * type in the barrel only (`X_FACTORY_NAME_SPELLING`), so `new` is never a second spelling.
295
+ */
296
+ export function memoryVectorStore(input: MemoryVectorStoreInput): MemoryVectorStore {
297
+ return new MemoryVectorStore(input);
298
+ }
299
+
250
300
  /**
251
301
  * Fuse ranked lists by reciprocal rank. Exported so a reranker can reuse it — which is why the
252
302
  * damping is screened HERE too and not only in `hybrid`: a default parameter is a bound like any
@@ -279,4 +329,20 @@ export function fuse(
279
329
  * sort then resolves them by dense-list insertion order, which no SQL engine reproduces.
280
330
  */
281
331
  const byScoreDesc = (a: SearchHit, b: SearchHit): number =>
282
- b.score - a.score || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
332
+ byScore(a.score, b.score) || byId(a.id, b.id);
333
+
334
+ /** The same order over a ranking's rows, which still carry their tenant. */
335
+ const byRankDesc = (a: Scored, b: Scored): number =>
336
+ byScore(a.score, b.score) || byId(a.record.id, b.record.id);
337
+
338
+ const byId = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
339
+
340
+ /**
341
+ * Descending, `NaN` last. A cosine against a zero-norm vector is `NaN`, and float8 orders `NaN`
342
+ * above every number, so pg's `order by distance` puts that row at the end. `b - a` alone is `NaN`
343
+ * for any pair holding one, which reads as a tie and leaves the sort inconsistent.
344
+ */
345
+ function byScore(a: number, b: number): number {
346
+ if (Number.isNaN(a) || Number.isNaN(b)) return Number.isNaN(a) ? (Number.isNaN(b) ? 0 : 1) : -1;
347
+ return b - a;
348
+ }
package/src/wire.ts CHANGED
@@ -185,9 +185,14 @@ export class MessageStream {
185
185
  }
186
186
  }
187
187
 
188
- /** True once `message_stop` arrived. False means the connection died mid-answer. */
188
+ /**
189
+ * True once `message_stop` arrived with every tool_use block closed. False means the connection
190
+ * died mid-answer — or the stream ended with a call's arguments still open, which drops the call
191
+ * (it is emitted whole or not at all), and for `llm()` that call IS the answer. `openai-wire.ts`
192
+ * refuses the same case.
193
+ */
189
194
  isComplete(): boolean {
190
- return this.stopped;
195
+ return this.stopped && this.pending.size === 0;
191
196
  }
192
197
 
193
198
  state(): StreamState {
@@ -1,35 +0,0 @@
1
- /**
2
- * The eval attached to `fixLinePrompt`. Cases live here rather than in the test file, matching
3
- * every app's own prompts — a fixture-driven, deterministic `exact` scorer, no judge, because
4
- * this is a proof of the eval mechanism itself and a proof that can drift is not one.
5
- */
6
-
7
- import { defineEval } from './evals';
8
- import { fixLinePrompt } from './fix-line';
9
- import { exact } from './scorers';
10
-
11
- export const fixLineCases = [
12
- {
13
- name: 'runnable/x-command',
14
- vars: { fixLine: 'bun run scripts/verify.ts --only lint,boundaries' },
15
- expected: 'runnable',
16
- },
17
- { name: 'runnable/cli-flag', vars: { fixLine: 'x db migrate' }, expected: 'runnable' },
18
- {
19
- name: 'vague/check',
20
- vars: { fixLine: 'check the configuration and try again' },
21
- expected: 'vague',
22
- },
23
- { name: 'vague/see-docs', vars: { fixLine: 'see the docs for details' }, expected: 'vague' },
24
- ];
25
-
26
- export const fixLineEval = defineEval({
27
- name: 'ai.fix-line-runnable',
28
- prompt: fixLinePrompt,
29
- // The gate is the drop from this recorded score, never an absolute number: models drift,
30
- // prompts should not. Accepting a new number is a diff in the committed baseline file.
31
- baseline: import.meta.resolve('./fix-line.v1.baseline.json'),
32
- tolerance: 0.05,
33
- scorers: [exact],
34
- cases: fixLineCases,
35
- });
package/src/fix-line.ts DELETED
@@ -1,27 +0,0 @@
1
- /**
2
- * The prompt behind `fix-line` — `@ultimat3/ai`'s own dogfood eval, and the package's first
3
- * framework-level `*.eval.test.ts`. Not an app feature: every app that declares a prompt is
4
- * required to pair it with an eval and a committed baseline (`defineEval`, `X_EVAL_MISSING`,
5
- * `X_EVAL_BASELINE_MISSING`), and this proves that whole convention actually catches a
6
- * regression from inside the package that owns it — through the real opt-in suite `x verify`'s
7
- * `eval` step runs, not only through `evals.test.ts`'s unit fixtures against a temp-dir baseline.
8
- *
9
- * The task is small on purpose: classify whether an error's `fix:` line is a runnable command —
10
- * axiom 4, "errors are instructions" — or vague guidance ("check the config", "see the docs").
11
- */
12
-
13
- import { definePrompt } from './prompt';
14
-
15
- export const fixLinePrompt = definePrompt<{ fixLine: string }>({
16
- id: 'ai.fix-line-runnable',
17
- version: '1',
18
- template:
19
- 'Reply with exactly one word: "runnable" if the fix line below names a command to run, or ' +
20
- '"vague" if it only describes what to do without naming one.\nFix line: {{fixLine}}',
21
- input: {
22
- type: 'object',
23
- properties: { fixLine: { type: 'string' } },
24
- required: ['fixLine'],
25
- },
26
- output: { type: 'string', enum: ['runnable', 'vague'] },
27
- });
@@ -1,84 +0,0 @@
1
- // The OpenAI-format built-in catalogue: the vendor's own list prices, registered through the same
2
- // public `registerModel` the Anthropic built-ins use. Here rather than in models.ts because these
3
- // rows belong to a PROVIDER — models.ts owns the registry mechanism, never one vendor's price list.
4
-
5
- import type { Money } from '@ultimat3/money';
6
- import type { ModelId } from './models';
7
- import { registerModel } from './models';
8
-
9
- /** A price per million tokens, in INTEGER MINOR UNITS. Same rule as models.ts: never a float. */
10
- const usd = (minor: number): Money => ({ minor, currency: 'USD' });
11
-
12
- /**
13
- * The ids this package prices, in ladder order (most capable first). A provider's `models` list is
14
- * still its own — an endpoint speaking this format serves whatever ids it was deployed with, and
15
- * `openAiProvider({ models })` is where those are named.
16
- */
17
- export const OPENAI_MODEL_IDS: readonly ModelId[] = [
18
- 'gpt-5.6-sol',
19
- 'gpt-5.6-terra',
20
- 'gpt-5.6-luna',
21
- ];
22
-
23
- /**
24
- * Shared by the whole 5.6 family: a 1.05M context, a 128k output ceiling, and `reasoning_effort`
25
- * over exactly the five rungs `EFFORTS` declares (the endpoint also takes `none`, which is what
26
- * `thinking: 'disabled'` maps onto). `adaptive` is false because the format has no
27
- * adaptive-thinking control at all — depth is `reasoning_effort` and nothing else.
28
- */
29
- const FAMILY = {
30
- /** One ladder: `moreCapableThan` compares these three with each other and with nothing else. */
31
- family: 'openai',
32
- contextWindow: 1_050_000,
33
- maxOutput: 128_000,
34
- /** Automatic caching starts at a 1024-token prefix; a shorter one silently does not cache. */
35
- cacheMinimumTokens: 1_024,
36
- reasoning: { effort: true, adaptive: false, disableThinkingUpTo: undefined },
37
- } as const;
38
-
39
- /**
40
- * The three models this package is confident enough to price, and no more.
41
- *
42
- * Prices are the vendor's published list, read from developers.openai.com/api/docs/pricing on
43
- * **2026-08-16**, in USD per million tokens:
44
- *
45
- * | id | input | cached input | output |
46
- * |---|---|---|---|
47
- * | `gpt-5.6-sol` | $5.00 | $0.50 | $30.00 |
48
- * | `gpt-5.6-terra` | $2.00 | $0.20 | $12.00 |
49
- * | `gpt-5.6-luna` | $0.20 | $0.02 | $1.20 |
50
- *
51
- * Deliberately not registered: `gpt-4o`, `gpt-4o-mini` and the `o1` family, whose cached input is
52
- * **0.5x** their input rate rather than the 0.1x `costOf` assumes — a spec that prices them would
53
- * under-report a cache-heavy workload by four fifths, and `costOf` answers confidently either way.
54
- * `gpt-5.5-pro` and `o1-pro` are out for the same class of reason: they publish no cached rate.
55
- * A wrong price is worse than no entry, so an app wanting one of those registers it itself, with
56
- * the rate its own contract names.
57
- */
58
- export function registerOpenAiModels(): void {
59
- registerModel({
60
- id: 'gpt-5.6-sol',
61
- ...FAMILY,
62
- inputPerMillion: usd(500),
63
- outputPerMillion: usd(3_000),
64
- });
65
- registerModel({
66
- id: 'gpt-5.6-terra',
67
- ...FAMILY,
68
- inputPerMillion: usd(200),
69
- outputPerMillion: usd(1_200),
70
- });
71
- registerModel({
72
- id: 'gpt-5.6-luna',
73
- ...FAMILY,
74
- inputPerMillion: usd(20),
75
- outputPerMillion: usd(120),
76
- });
77
- }
78
-
79
- // The same shape models.ts uses for its built-ins: registration is a module side effect, so the
80
- // default path is the app's path and importing the provider is enough to price what it serves.
81
- // Exported as well as called, because the registry is module state and a suite that clears it with
82
- // `resetModels()` otherwise leaves this provider serving ids nothing can price. Re-registering
83
- // REPLACES, so an app with a negotiated rate calls `registerModel` after this one and wins.
84
- registerOpenAiModels();