@kaana.ai/contracts 8.5.0 → 8.7.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.
@@ -33,34 +33,44 @@ export const inferenceContentSourceSchema = z.discriminatedUnion('kind', [
33
33
  /**
34
34
  * One part of a message's content. A message is always a list of parts.
35
35
  *
36
- * ## `refusal` is a member and `reasoning` is not, and the asymmetry is the point
36
+ * ## `refusal` and `reasoning` are members, and neither is ever answer text
37
37
  *
38
38
  * A model that DECLINES says why, and those words are meant for the customer:
39
39
  * they are the difference between "rephrase this" and "stop asking". Before this
40
40
  * member existed there was nowhere in an {@link InferenceMessage} to put them, so
41
- * a non-streaming fold kept `finishReason: 'refusal'` and dropped the sentence —
42
- * the customer learned they were refused and not why, while a streaming caller of
43
- * the same request got the whole explanation on the `refusal` delta channel.
41
+ * a non-streaming fold kept `finishReason: 'refusal'` and dropped the sentence.
42
+ * The part is a MEMBER rather than a field because a refusal need not have text:
43
+ * an Anthropic `stop_reason: "refusal"` maps to the finish reason and separates
44
+ * no words from the answer, while an OpenAI-compatible `refusal` does.
44
45
  *
45
- * Reasoning is the opposite case and stays absent. It is the model's private
46
- * working, and a `text` part is where somebody would put it — which renders
47
- * private reasoning to the customer AS the answer, the product bug the delta
48
- * channels exist to prevent. An opaque per-block reasoning blob crossing this
49
- * boundary needs a home nobody has chosen yet, and inventing one here would
50
- * choose it by accident.
46
+ * ## `reasoning` (contract set 8.6.0, Kaana ADR 0009 decision 2)
51
47
  *
52
- * Both public dialects can carry a refusal, which is what separates the two
53
- * cases at the boundary as well as in principle: `refusal` is OpenAI's OWN field
54
- * in both of its shapes (`delta.refusal` streaming, `message.refusal`
55
- * non-streaming), while reasoning has no OpenAI field at all — the
56
- * `reasoning_content`/`reasoning` spellings an OpenAI-compatible provider emits
57
- * are provider extensions. So carrying the refusal costs no dialect its
58
- * standard-client parseability, and carrying reasoning would.
48
+ * Reasoning was deliberately absent until the owner chose its home, because an
49
+ * opaque per-block reasoning blob had none. The cost of that absence was
50
+ * correctness: a thinking model that calls a tool cannot be continued unless the
51
+ * assistant's reasoning blocks, with their signatures, come back to the provider
52
+ * unmodified and in order, and a customer who only ever received reasoning TEXT
53
+ * on the `reasoning` delta channel had nothing to send back.
59
54
  *
60
- * The part is a MEMBER rather than a field because a refusal need not have text:
61
- * an Anthropic `stop_reason: "refusal"` maps to the finish reason and separates
62
- * no words from the answer, while an OpenAI-compatible `refusal` does. A required
63
- * field would force the first provider to invent a sentence.
55
+ * The part is `{type: 'reasoning', text?, opaque?}`:
56
+ *
57
+ * - `text` is what a person may read. It is a display value and is never
58
+ * forwarded to a provider on its own.
59
+ * - `opaque` is a continuation token the customer MUST return UNMODIFIED, in the
60
+ * position it arrived. Its content is Kaana's to define and the customer's to
61
+ * treat as a sealed string: it is provider-neutral on the surface and carries
62
+ * whatever the serving provider needs to continue. Sealing is NOT
63
+ * cryptographic: a forged or edited token is no more than a request the
64
+ * provider refuses, so nothing may rely on it for trust.
65
+ * - at least one of the two is present.
66
+ *
67
+ * A reasoning part is the assistant's alone, exactly as a refusal is. It is
68
+ * never a `text` part, so no renderer can present private reasoning as the
69
+ * answer, and the delta channels keep reasoning apart for the same reason.
70
+ *
71
+ * What a route does with a token it cannot read (it was issued by another
72
+ * provider family, after a failover or a model switch) is drop it, never fail:
73
+ * the continuation is lost, the conversation is not.
64
74
  */
65
75
  export const inferenceContentPartSchema = z.discriminatedUnion('type', [
66
76
  z.object({ type: z.literal('text'), text: z.string() }).strict(),
@@ -85,6 +95,20 @@ export const inferenceContentPartSchema = z.discriminatedUnion('type', [
85
95
  * renderer can present a refusal as the answer that was asked for.
86
96
  */
87
97
  z.object({ type: z.literal('refusal'), text: z.string() }).strict(),
98
+ /**
99
+ * The assistant's reasoning, readable and/or as an opaque continuation token
100
+ * to return unmodified. See the member note above.
101
+ */
102
+ z
103
+ .object({
104
+ type: z.literal('reasoning'),
105
+ text: z.string().optional(),
106
+ opaque: z.string().check(minUtf16Length(1)).optional(),
107
+ })
108
+ .strict()
109
+ .refine((part) => part.text !== undefined || part.opaque !== undefined, {
110
+ message: 'a reasoning part carries text, an opaque token, or both',
111
+ }),
88
112
  ]);
89
113
  /* -------------------------------------------------------------------------- */
90
114
  /* Image input (contract set 8.2.0, Kaana ADR 0007) */
@@ -257,15 +281,15 @@ export const inferenceMessageSchema = z
257
281
  });
258
282
  }
259
283
  // Same rule as `toolCalls`, for the same reason: only the assistant can
260
- // decline, so a refusal on any other role is a part every provider would
261
- // silently ignore — and one a renderer might not.
284
+ // decline or reason, so either part on any other role is one every provider
285
+ // would silently ignore — and one a renderer might not.
262
286
  if (message.role !== 'assistant') {
263
287
  for (const [index, part] of message.content.entries()) {
264
- if (part.type === 'refusal') {
288
+ if (part.type === 'refusal' || part.type === 'reasoning') {
265
289
  ctx.addIssue({
266
290
  code: 'custom',
267
291
  path: ['content', index, 'type'],
268
- message: 'only an assistant message carries a refusal',
292
+ message: `only an assistant message carries a ${part.type}`,
269
293
  });
270
294
  }
271
295
  }
@@ -327,12 +351,33 @@ export const responseFormatSchema = z.discriminatedUnion('type', [
327
351
  ]);
328
352
  /**
329
353
  * The caller's reasoning control. Strict, like every leaf: a provider-specific
330
- * knob (`budget_tokens`, `summary`) is refused rather than silently dropped.
354
+ * knob (`summary`) is refused rather than silently dropped.
355
+ *
356
+ * - `effort` is how much to reason, in the catalogue's words. Oxy forwards it
357
+ * only after checking the resolved model advertises it in `reasoningEfforts`.
358
+ * - `maxTokens` is a thinking BUDGET in tokens. It is the other way to size
359
+ * reasoning, so it excludes `effort`. A route that cannot take a budget
360
+ * refuses it with `invalid_request` naming `reasoning.maxTokens`.
361
+ * - `exclude: true` means the model may still reason, and be billed for it, but
362
+ * no reasoning is returned: no `reasoning` delta, no reasoning part, no opaque
363
+ * token. A caller that excludes reasoning cannot continue it.
331
364
  *
332
- * Oxy forwards it only after checking the resolved model advertises the effort
333
- * in its catalogue `reasoningEfforts`; Kaana translates it per provider.
365
+ * Carrying `reasoning` at all, even as `{}`, opts in to receiving reasoning
366
+ * continuation tokens (the `opaque` field of a `reasoning` delta); a request
367
+ * without it never receives one, which is what keeps older readers of the
368
+ * stream unaffected.
334
369
  */
335
- export const inferenceReasoningSchema = z.object({ effort: reasoningEffortSchema }).strict();
370
+ export const inferenceReasoningSchema = z
371
+ .object({
372
+ effort: reasoningEffortSchema.optional(),
373
+ maxTokens: z.number().int().min(1).max(2_147_483_647).optional(),
374
+ exclude: z.boolean().optional(),
375
+ })
376
+ .strict()
377
+ .refine((reasoning) => reasoning.effort === undefined || reasoning.maxTokens === undefined, {
378
+ message: 'reasoning takes an effort or a maxTokens budget, not both',
379
+ path: ['maxTokens'],
380
+ });
336
381
  /** Parameters for text-to-speech, preserved in the signed request. */
337
382
  export const inferenceSpeechParametersSchema = z
338
383
  .object({
@@ -92,6 +92,10 @@ export declare const responsesRequestSchema: z.ZodObject<{
92
92
  }, z.core.$strict>, z.ZodObject<{
93
93
  type: z.ZodLiteral<"refusal">;
94
94
  text: z.ZodString;
95
+ }, z.core.$strict>, z.ZodObject<{
96
+ type: z.ZodLiteral<"reasoning">;
97
+ text: z.ZodOptional<z.ZodString>;
98
+ opaque: z.ZodOptional<z.ZodString>;
95
99
  }, z.core.$strict>], "type">>;
96
100
  name: z.ZodOptional<z.ZodString>;
97
101
  toolCallId: z.ZodOptional<z.ZodString>;
@@ -136,11 +140,13 @@ export declare const responsesRequestSchema: z.ZodObject<{
136
140
  strict: z.ZodBoolean;
137
141
  }, z.core.$strict>], "type">>;
138
142
  reasoning: z.ZodOptional<z.ZodObject<{
139
- effort: z.ZodEnum<{
143
+ effort: z.ZodOptional<z.ZodEnum<{
140
144
  low: "low";
141
145
  medium: "medium";
142
146
  high: "high";
143
- }>;
147
+ }>>;
148
+ maxTokens: z.ZodOptional<z.ZodNumber>;
149
+ exclude: z.ZodOptional<z.ZodBoolean>;
144
150
  }, z.core.$strict>>;
145
151
  labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
146
152
  clientRequestId: z.ZodOptional<z.ZodString>;
@@ -240,6 +246,10 @@ export declare const responseSchema: z.ZodObject<{
240
246
  }, z.core.$strict>, z.ZodObject<{
241
247
  type: z.ZodLiteral<"refusal">;
242
248
  text: z.ZodString;
249
+ }, z.core.$strict>, z.ZodObject<{
250
+ type: z.ZodLiteral<"reasoning">;
251
+ text: z.ZodOptional<z.ZodString>;
252
+ opaque: z.ZodOptional<z.ZodString>;
243
253
  }, z.core.$strict>], "type">>;
244
254
  name: z.ZodOptional<z.ZodString>;
245
255
  toolCallId: z.ZodOptional<z.ZodString>;
@@ -25,6 +25,7 @@ export declare const inferenceStreamDeltaEventSchema: z.ZodObject<{
25
25
  output_audio_transcript: "output_audio_transcript";
26
26
  }>;
27
27
  text: z.ZodString;
28
+ opaque: z.ZodOptional<z.ZodString>;
28
29
  }, z.core.$strip>;
29
30
  /** The encodings an audio output may carry. */
30
31
  export declare const inferenceAudioMediaTypeSchema: z.ZodEnum<{
@@ -238,6 +239,7 @@ export declare const responseStreamEventSchema: z.ZodDiscriminatedUnion<[z.ZodOb
238
239
  output_audio_transcript: "output_audio_transcript";
239
240
  }>;
240
241
  text: z.ZodString;
242
+ opaque: z.ZodOptional<z.ZodString>;
241
243
  }, z.core.$strip>, z.ZodObject<{
242
244
  schemaVersion: z.ZodLiteral<1>;
243
245
  type: z.ZodLiteral<"audio">;
@@ -32,6 +32,14 @@ export const inferenceStreamDeltaEventSchema = z.object({
32
32
  */
33
33
  channel: z.enum(['output_text', 'reasoning', 'refusal', 'output_audio_transcript']),
34
34
  text: z.string(),
35
+ /**
36
+ * Contract set 8.6.0. On the `reasoning` channel only: the continuation token
37
+ * of one finished reasoning block, which the customer returns UNMODIFIED as the
38
+ * `opaque` of a `reasoning` content part, in the order the blocks arrived.
39
+ * Emitted once per block, after the block's text deltas (with empty `text`),
40
+ * and only to a request that carried `reasoning` and did not `exclude` it.
41
+ */
42
+ opaque: z.string().check(minUtf16Length(1)).optional(),
35
43
  });
36
44
  /** The encodings an audio output may carry. */
37
45
  export const inferenceAudioMediaTypeSchema = z.enum([
@@ -100,7 +100,7 @@
100
100
  * change to, say, the catalogue reject every in-flight inference request; the
101
101
  * per-shape `schemaVersion` is what a message is validated against.
102
102
  */
103
- export declare const INFERENCE_CONTRACT_VERSION = "8.5.0";
103
+ export declare const INFERENCE_CONTRACT_VERSION = "8.7.0";
104
104
  /**
105
105
  * Contract set 4.0.0 (Kaana ADR 0002) is the first published as
106
106
  * `@kaana.ai/contracts`. MAJOR because the set changed hands and split in two:
@@ -214,6 +214,15 @@ export declare const INFERENCE_CONTRACT_VERSION = "8.5.0";
214
214
  * `promptCacheKey`) on `POST /v1/chat/completions` and `POST /v1/responses`
215
215
  * (Kaana ADR 0008). The preference only narrows what the grant admits. Nothing
216
216
  * in 8.4.0 changes and the signed grant and envelope are untouched.
217
+ *
218
+ * Contract set 8.6.0 is additive: reasoning continuity (Kaana ADR 0009 decision
219
+ * 2). `reasoning` takes `maxTokens` and `exclude` beside `effort` (all optional;
220
+ * `effort` and `maxTokens` are exclusive), a message's content gains the
221
+ * assistant-only `reasoning` part (`text?`, `opaque?`), and a `reasoning` delta
222
+ * may carry `opaque`. Every 8.5.0 message parses the same; the delta's `opaque`
223
+ * is emitted only to a request that carried `reasoning`. A consumer on 8.5.0 or
224
+ * earlier (Oxy's legacy signed lane) refuses a message or request that carries
225
+ * what is new, so Oxy reads it before a customer sends it.
217
226
  */
218
227
  /** The public API's one origin and version. */
219
228
  export declare const KAANA_API_BASE_URL = "https://kaana.ai/v1";
@@ -100,7 +100,7 @@
100
100
  * change to, say, the catalogue reject every in-flight inference request; the
101
101
  * per-shape `schemaVersion` is what a message is validated against.
102
102
  */
103
- export const INFERENCE_CONTRACT_VERSION = '8.5.0';
103
+ export const INFERENCE_CONTRACT_VERSION = '8.7.0';
104
104
  /**
105
105
  * Contract set 4.0.0 (Kaana ADR 0002) is the first published as
106
106
  * `@kaana.ai/contracts`. MAJOR because the set changed hands and split in two:
@@ -214,6 +214,15 @@ export const INFERENCE_CONTRACT_VERSION = '8.5.0';
214
214
  * `promptCacheKey`) on `POST /v1/chat/completions` and `POST /v1/responses`
215
215
  * (Kaana ADR 0008). The preference only narrows what the grant admits. Nothing
216
216
  * in 8.4.0 changes and the signed grant and envelope are untouched.
217
+ *
218
+ * Contract set 8.6.0 is additive: reasoning continuity (Kaana ADR 0009 decision
219
+ * 2). `reasoning` takes `maxTokens` and `exclude` beside `effort` (all optional;
220
+ * `effort` and `maxTokens` are exclusive), a message's content gains the
221
+ * assistant-only `reasoning` part (`text?`, `opaque?`), and a `reasoning` delta
222
+ * may carry `opaque`. Every 8.5.0 message parses the same; the delta's `opaque`
223
+ * is emitted only to a request that carried `reasoning`. A consumer on 8.5.0 or
224
+ * earlier (Oxy's legacy signed lane) refuses a message or request that carries
225
+ * what is new, so Oxy reads it before a customer sends it.
217
226
  */
218
227
  /** The public API's one origin and version. */
219
228
  export const KAANA_API_BASE_URL = 'https://kaana.ai/v1';