@providerkit/core 0.17.2 → 0.20.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.
Files changed (68) hide show
  1. package/README.md +1 -0
  2. package/dist/auth/device.d.ts +34 -0
  3. package/dist/auth/device.d.ts.map +1 -0
  4. package/dist/auth/device.js +85 -0
  5. package/dist/auth/device.js.map +1 -0
  6. package/dist/auth/flows.d.ts +36 -0
  7. package/dist/auth/flows.d.ts.map +1 -0
  8. package/dist/auth/flows.js +392 -0
  9. package/dist/auth/flows.js.map +1 -0
  10. package/dist/auth/index.d.ts +5 -0
  11. package/dist/auth/index.d.ts.map +1 -0
  12. package/dist/auth/index.js +8 -0
  13. package/dist/auth/index.js.map +1 -0
  14. package/dist/auth/oauth.d.ts +92 -0
  15. package/dist/auth/oauth.d.ts.map +1 -0
  16. package/dist/auth/oauth.js +148 -0
  17. package/dist/auth/oauth.js.map +1 -0
  18. package/dist/auth/token-source.d.ts +30 -0
  19. package/dist/auth/token-source.d.ts.map +1 -0
  20. package/dist/auth/token-source.js +61 -0
  21. package/dist/auth/token-source.js.map +1 -0
  22. package/dist/context.d.ts.map +1 -1
  23. package/dist/context.js +17 -1
  24. package/dist/context.js.map +1 -1
  25. package/dist/errors.d.ts.map +1 -1
  26. package/dist/errors.js +3 -0
  27. package/dist/errors.js.map +1 -1
  28. package/dist/presets.d.ts +32 -3
  29. package/dist/presets.d.ts.map +1 -1
  30. package/dist/presets.js +24 -2
  31. package/dist/presets.js.map +1 -1
  32. package/dist/providers/anthropic.d.ts.map +1 -1
  33. package/dist/providers/anthropic.js +6 -1
  34. package/dist/providers/anthropic.js.map +1 -1
  35. package/dist/providers/factory.d.ts +8 -2
  36. package/dist/providers/factory.d.ts.map +1 -1
  37. package/dist/providers/factory.js +11 -4
  38. package/dist/providers/factory.js.map +1 -1
  39. package/dist/providers/gemini.d.ts.map +1 -1
  40. package/dist/providers/gemini.js +3 -0
  41. package/dist/providers/gemini.js.map +1 -1
  42. package/dist/providers/openai.d.ts +7 -1
  43. package/dist/providers/openai.d.ts.map +1 -1
  44. package/dist/providers/openai.js +6 -0
  45. package/dist/providers/openai.js.map +1 -1
  46. package/dist/providers/responses.d.ts +7 -1
  47. package/dist/providers/responses.d.ts.map +1 -1
  48. package/dist/providers/responses.js +48 -6
  49. package/dist/providers/responses.js.map +1 -1
  50. package/dist/types.d.ts +26 -0
  51. package/dist/types.d.ts.map +1 -1
  52. package/dist/types.js +13 -5
  53. package/dist/types.js.map +1 -1
  54. package/package.json +5 -1
  55. package/src/auth/device.ts +137 -0
  56. package/src/auth/flows.ts +526 -0
  57. package/src/auth/index.ts +7 -0
  58. package/src/auth/oauth.ts +226 -0
  59. package/src/auth/token-source.ts +76 -0
  60. package/src/context.ts +16 -1
  61. package/src/errors.ts +3 -0
  62. package/src/presets.ts +33 -4
  63. package/src/providers/anthropic.ts +10 -1
  64. package/src/providers/factory.ts +21 -6
  65. package/src/providers/gemini.ts +7 -0
  66. package/src/providers/openai.ts +12 -0
  67. package/src/providers/responses.ts +60 -6
  68. package/src/types.ts +49 -5
@@ -20,6 +20,7 @@ import type {
20
20
  ImagePart,
21
21
  Provider,
22
22
  ProviderChunk,
23
+ ServiceTier,
23
24
  StreamOptions,
24
25
  ToolDefinition,
25
26
  } from "../types.ts";
@@ -56,6 +57,12 @@ export interface ResponsesConfig extends ProviderFallbackConfig {
56
57
  /** What this backend accepts as an image. Each image it would refuse is
57
58
  * replaced by a note naming the reason, instead of failing the request. */
58
59
  imageLimits?: ImageLimits;
60
+ /** Ask for the encrypted reasoning items and replay them on the next turn,
61
+ * so the model keeps its chain of thought across a stateless turn. Only for
62
+ * backends that accept `include: ["reasoning.encrypted_content"]`. */
63
+ replayReasoning?: boolean;
64
+ /** Default `service_tier` for every call; `StreamOptions.serviceTier` wins. */
65
+ serviceTier?: ServiceTier;
59
66
  }
60
67
 
61
68
  const DEFAULT_BASE_URL = "https://api.openai.com";
@@ -91,7 +98,33 @@ type ResponsesContentPart =
91
98
  | { type: "output_text"; text: string }
92
99
  | { type: "input_image"; image_url: string };
93
100
 
101
+ /** A reasoning item the backend made, sent back as it came. */
102
+ type ReplayedReasoning = { type: "reasoning" } & Record<string, unknown>;
103
+
104
+ function isReplayedReasoning(item: unknown): item is ReplayedReasoning {
105
+ return typeof item === "object" && item !== null && "type" in item && item.type === "reasoning";
106
+ }
107
+
108
+ /** The most encrypted reasoning one request carries back. Newest turns win. */
109
+ const MAX_REPLAY_BYTES = 8 * 1024 * 1024;
110
+
111
+ /** Which assistant messages get their reasoning replayed: this provider's own,
112
+ * newest first, until the cap. An older turn's reasoning is the cheapest to lose. */
113
+ function replayable(messages: readonly ChatMessage[], provider: string): Set<ChatMessage> {
114
+ const keep = new Set<ChatMessage>();
115
+ let bytes = 0;
116
+ for (let i = messages.length - 1; i >= 0; i--) {
117
+ const message = messages[i]!;
118
+ if (message.role !== "assistant" || message.reasoningItems?.provider !== provider) continue;
119
+ bytes += JSON.stringify(message.reasoningItems.items).length;
120
+ if (bytes > MAX_REPLAY_BYTES) break;
121
+ keep.add(message);
122
+ }
123
+ return keep;
124
+ }
125
+
94
126
  type ResponsesInputItem =
127
+ | ReplayedReasoning
95
128
  | { type: "message"; role: "user" | "assistant"; content: ResponsesContentPart[] }
96
129
  | { type: "function_call"; call_id: string; name: string; arguments: string }
97
130
  | { type: "function_call_output"; call_id: string; output: string | ResponsesContentPart[] };
@@ -141,6 +174,7 @@ export function toResponsesInput(
141
174
  .map((message) => message.content)
142
175
  .join("\n\n");
143
176
 
177
+ const replayed = replayable(messages, provider);
144
178
  const input: ResponsesInputItem[] = [];
145
179
  for (const message of messages) {
146
180
  switch (message.role) {
@@ -167,12 +201,15 @@ export function toResponsesInput(
167
201
  break;
168
202
 
169
203
  case "assistant": {
170
- // Reasoning is deliberately NOT replayed. This shape wants the ORIGINAL
171
- // reasoning item back — its `rs_…` id, and under `store: false` its
172
- // `encrypted_content` blob — and the seam carries neither, only the
173
- // plain summary text a caller renders. A synthesized reasoning item is
174
- // rejected; omitting it costs only the model re-deriving its own chain
175
- // of thought, which is what every stateless caller already lives with.
204
+ // `reasoning` (the summary a caller renders) is never replayed: this
205
+ // shape wants the ORIGINAL item back, `rs_…` id and `encrypted_content`
206
+ // blob included, and a synthesized one is rejected. `reasoningItems`
207
+ // are those originals, so they go back first, but only to the provider
208
+ // that made them. Without them the model re-derives its own chain of
209
+ // thought, which is what every stateless caller already lives with.
210
+ if (replayed.has(message)) {
211
+ input.push(...message.reasoningItems!.items.filter(isReplayedReasoning));
212
+ }
176
213
  if (message.content) {
177
214
  input.push({
178
215
  type: "message",
@@ -213,6 +250,7 @@ interface ResponsesItem {
213
250
  call_id?: string;
214
251
  name?: string;
215
252
  arguments?: string;
253
+ encrypted_content?: string;
216
254
  }
217
255
 
218
256
  interface ResponsesEvent {
@@ -350,6 +388,7 @@ export function createResponsesProvider(config: ResponsesConfig): Provider {
350
388
  store: false,
351
389
  };
352
390
  if (instructions) request.instructions = instructions;
391
+ if (config.replayReasoning) request.include = ["reasoning.encrypted_content"];
353
392
  const maxTokens = opts.maxTokens ?? config.maxTokens;
354
393
  if (maxTokens !== undefined) request.max_output_tokens = maxTokens;
355
394
  if (opts.temperature !== undefined) request.temperature = opts.temperature;
@@ -386,6 +425,8 @@ export function createResponsesProvider(config: ResponsesConfig): Provider {
386
425
  // The cache affinity key. Without one the backend spreads a
387
426
  // conversation's turns across cache shards, and a re-sent prefix misses.
388
427
  if (opts.sessionId) request.prompt_cache_key = opts.sessionId;
428
+ const serviceTier = opts.serviceTier ?? config.serviceTier;
429
+ if (serviceTier) request.service_tier = serviceTier;
389
430
  // Muse Contributor accepts only `auto`. `none` has an exact wire-level
390
431
  // equivalent: send no tools. Required/named choices stay explicit and let
391
432
  // the endpoint refuse a promise it cannot keep.
@@ -421,6 +462,11 @@ export function createResponsesProvider(config: ResponsesConfig): Provider {
421
462
  let sawToolCall = false;
422
463
  let wall: ProviderError | undefined;
423
464
  let produced = false;
465
+ const reasoningItems: unknown[] = [];
466
+ const reasoningChunk = (): ProviderChunk[] =>
467
+ reasoningItems.length
468
+ ? [{ type: "delta", reasoningItems: { provider: id, items: reasoningItems } }]
469
+ : [];
424
470
 
425
471
  for await (const data of streamSse({
426
472
  url: apiUrl(baseUrl, config.path ?? DEFAULT_PATH),
@@ -522,6 +568,12 @@ export function createResponsesProvider(config: ResponsesConfig): Provider {
522
568
 
523
569
  case "response.output_item.done": {
524
570
  const item = event.item;
571
+ // Kept whole and emitted once, at the end of the turn: it is the
572
+ // backend's own record, and it only means something complete.
573
+ if (config.replayReasoning && item?.type === "reasoning") {
574
+ reasoningItems.push(item);
575
+ break;
576
+ }
525
577
  if (item?.type !== "function_call") break;
526
578
  const known = pendingFor(item.id, item.call_id);
527
579
  if (item.id) pending.delete(item.id);
@@ -583,6 +635,7 @@ export function createResponsesProvider(config: ResponsesConfig): Provider {
583
635
  if (response?.status === "failed" || response?.error) {
584
636
  throw streamError(id, response.error ?? { message: "response failed" });
585
637
  }
638
+ yield* reasoningChunk();
586
639
  if (response?.usage) yield usageChunk(response.usage);
587
640
  if (response?.status === "incomplete") {
588
641
  yield {
@@ -602,6 +655,7 @@ export function createResponsesProvider(config: ResponsesConfig): Provider {
602
655
  case "response.incomplete": {
603
656
  // A turn that ran out of output tokens still billed for its input,
604
657
  // and this event carries usage in the same shape as `completed`.
658
+ yield* reasoningChunk();
605
659
  const usage = event.response?.usage;
606
660
  if (usage) yield usageChunk(usage);
607
661
  yield {
package/src/types.ts CHANGED
@@ -108,6 +108,14 @@ export type ChatMessage =
108
108
  * continuity across a tool round. Absent on every other dialect.
109
109
  */
110
110
  reasoningDetails?: unknown[];
111
+ /**
112
+ * The ChatGPT backend's own reasoning items, encrypted, riding back
113
+ * UNMODIFIED on the next turn so the model keeps its chain of thought.
114
+ * Tagged with the provider that made them: only that provider replays
115
+ * them, because another vendor (after a fallback) would reject them.
116
+ * Opaque, like `reasoningDetails`.
117
+ */
118
+ reasoningItems?: ReasoningItems;
111
119
  toolCalls?: ToolCall[];
112
120
  }
113
121
  | {
@@ -119,6 +127,12 @@ export type ChatMessage =
119
127
  images?: ImagePart[];
120
128
  };
121
129
 
130
+ /** Provider-owned reasoning items and the id of the provider that made them. */
131
+ export interface ReasoningItems {
132
+ provider: string;
133
+ items: unknown[];
134
+ }
135
+
122
136
  /** JSON Schema for an object — what every provider's tool contract wants. */
123
137
  export interface JsonObjectSchema {
124
138
  type: "object";
@@ -192,6 +206,8 @@ export interface ProviderChunk {
192
206
  /** OpenRouter's normalized reasoning payload — hand it back on the next
193
207
  * turn's assistant message verbatim. See ChatMessage.reasoningDetails. */
194
208
  reasoningDetails?: unknown[];
209
+ /** Once per turn, where a provider replays its reasoning. See ChatMessage.reasoningItems. */
210
+ reasoningItems?: ReasoningItems;
195
211
  toolCalls?: ToolCallDelta[];
196
212
  usage?: TokenUsage;
197
213
  finishReason?: FinishReason;
@@ -387,8 +403,19 @@ export interface StreamOptions {
387
403
  * so it refuses `false` rather than silently ignore it.
388
404
  */
389
405
  parallelToolCalls?: boolean;
406
+ /**
407
+ * Which service tier to buy this call on: `priority` is faster and costs
408
+ * more, `flex` is slower and costs less. Sent as `service_tier` on the
409
+ * OpenAI and Responses shapes. Anthropic and Gemini refuse it with an
410
+ * `invalid` error: Anthropic's own `service_tier` means something else, and
411
+ * Gemini has no such switch. Overrides the provider's own setting.
412
+ */
413
+ serviceTier?: ServiceTier;
390
414
  }
391
415
 
416
+ // ponytail: Codex's `ultrafast` tier (gpt-6-astra only) is the next value to add.
417
+ export type ServiceTier = "priority" | "flex";
418
+
392
419
  export interface Provider {
393
420
  readonly id: string;
394
421
  readonly model: string;
@@ -406,6 +433,8 @@ export interface Completion {
406
433
  reasoning: string;
407
434
  /** Present only where the provider sent one. See ChatMessage.reasoningDetails. */
408
435
  reasoningDetails?: unknown[];
436
+ /** Present only where the provider sent them. See ChatMessage.reasoningItems. */
437
+ reasoningItems?: ReasoningItems;
409
438
  usage: TokenUsage;
410
439
  finishReason: FinishReason | null;
411
440
  model: string;
@@ -424,6 +453,7 @@ export async function drainStream(
424
453
  // a drained turn replays only half its own reasoning on the next round —
425
454
  // which is the failure `reasoningDetails` exists to prevent.
426
455
  let reasoningDetails: unknown[] | undefined;
456
+ let reasoningItems: ReasoningItems | undefined;
427
457
  let usage: TokenUsage = EMPTY_USAGE;
428
458
  let finishReason: FinishReason | null = null;
429
459
  let source: ProviderChunk["source"];
@@ -433,6 +463,7 @@ export async function drainStream(
433
463
  if (chunk.content) text += chunk.content;
434
464
  if (chunk.reasoning) reasoning += chunk.reasoning;
435
465
  if (chunk.reasoningDetails?.length) reasoningDetails = chunk.reasoningDetails;
466
+ if (chunk.reasoningItems?.items.length) reasoningItems = chunk.reasoningItems;
436
467
  } else if (chunk.type === "usage" && chunk.usage) {
437
468
  usage = chunk.usage;
438
469
  } else if (chunk.type === "finish" && chunk.finishReason) {
@@ -443,6 +474,7 @@ export async function drainStream(
443
474
  text,
444
475
  reasoning,
445
476
  ...(reasoningDetails ? { reasoningDetails } : {}),
477
+ ...(reasoningItems ? { reasoningItems } : {}),
446
478
  usage,
447
479
  finishReason,
448
480
  model: source?.model ?? model,
@@ -465,11 +497,23 @@ export async function drainStream(
465
497
  export function stripReasoning(messages: readonly ChatMessage[]): ChatMessage[] {
466
498
  return messages.map((message) => {
467
499
  if (message.role !== "assistant") return message;
468
- if (message.reasoning === undefined && message.reasoningDetails === undefined) return message;
469
- // Both halves go. `reasoningDetails` is the same chain of thought in the
470
- // provider's own words, so leaving it behind carries into a thinking-off
471
- // turn exactly what stripping `reasoning` was meant to keep out.
472
- const { reasoning: _text, reasoningDetails: _payload, ...rest } = message;
500
+ if (
501
+ message.reasoning === undefined &&
502
+ message.reasoningDetails === undefined &&
503
+ message.reasoningItems === undefined
504
+ ) {
505
+ return message;
506
+ }
507
+ // Every half goes. `reasoningDetails` and `reasoningItems` are the same
508
+ // chain of thought in the provider's own words, so leaving them behind
509
+ // carries into a thinking-off turn exactly what stripping `reasoning` was
510
+ // meant to keep out.
511
+ const {
512
+ reasoning: _text,
513
+ reasoningDetails: _payload,
514
+ reasoningItems: _items,
515
+ ...rest
516
+ } = message;
473
517
  return rest;
474
518
  });
475
519
  }