@providerkit/core 0.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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +245 -0
  3. package/dist/context.d.ts +69 -0
  4. package/dist/context.d.ts.map +1 -0
  5. package/dist/context.js +132 -0
  6. package/dist/context.js.map +1 -0
  7. package/dist/errors.d.ts +86 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/errors.js +356 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/index.d.ts +13 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +13 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/providers/anthropic.d.ts +26 -0
  16. package/dist/providers/anthropic.d.ts.map +1 -0
  17. package/dist/providers/anthropic.js +245 -0
  18. package/dist/providers/anthropic.js.map +1 -0
  19. package/dist/providers/openai.d.ts +30 -0
  20. package/dist/providers/openai.d.ts.map +1 -0
  21. package/dist/providers/openai.js +185 -0
  22. package/dist/providers/openai.js.map +1 -0
  23. package/dist/retry.d.ts +79 -0
  24. package/dist/retry.d.ts.map +1 -0
  25. package/dist/retry.js +200 -0
  26. package/dist/retry.js.map +1 -0
  27. package/dist/schema.d.ts +2 -0
  28. package/dist/schema.d.ts.map +1 -0
  29. package/dist/schema.js +48 -0
  30. package/dist/schema.js.map +1 -0
  31. package/dist/tool-args.d.ts +12 -0
  32. package/dist/tool-args.d.ts.map +1 -0
  33. package/dist/tool-args.js +113 -0
  34. package/dist/tool-args.js.map +1 -0
  35. package/dist/tools.d.ts +82 -0
  36. package/dist/tools.d.ts.map +1 -0
  37. package/dist/tools.js +155 -0
  38. package/dist/tools.js.map +1 -0
  39. package/dist/transport.d.ts +31 -0
  40. package/dist/transport.d.ts.map +1 -0
  41. package/dist/transport.js +157 -0
  42. package/dist/transport.js.map +1 -0
  43. package/dist/types.d.ts +168 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +75 -0
  46. package/dist/types.js.map +1 -0
  47. package/dist/usage.d.ts +50 -0
  48. package/dist/usage.d.ts.map +1 -0
  49. package/dist/usage.js +71 -0
  50. package/dist/usage.js.map +1 -0
  51. package/dist/watchdog.d.ts +34 -0
  52. package/dist/watchdog.d.ts.map +1 -0
  53. package/dist/watchdog.js +85 -0
  54. package/dist/watchdog.js.map +1 -0
  55. package/dist/zod.d.ts +32 -0
  56. package/dist/zod.d.ts.map +1 -0
  57. package/dist/zod.js +49 -0
  58. package/dist/zod.js.map +1 -0
  59. package/package.json +76 -0
  60. package/src/context.ts +150 -0
  61. package/src/errors.ts +398 -0
  62. package/src/index.ts +12 -0
  63. package/src/providers/anthropic.ts +315 -0
  64. package/src/providers/openai.ts +246 -0
  65. package/src/retry.ts +246 -0
  66. package/src/schema.ts +67 -0
  67. package/src/tool-args.ts +117 -0
  68. package/src/tools.ts +237 -0
  69. package/src/transport.ts +162 -0
  70. package/src/types.ts +231 -0
  71. package/src/usage.ts +106 -0
  72. package/src/watchdog.ts +119 -0
  73. package/src/zod.ts +74 -0
@@ -0,0 +1,85 @@
1
+ // The stream-idle watchdog.
2
+ //
3
+ // A provider that stops sending bytes is indistinguishable from a long prefill
4
+ // — except that it never ends, and every SDK's default is to wait forever. A
5
+ // queued route or a wedged prefill upstream hangs the caller indefinitely, and
6
+ // the symptom is the worst kind: nothing. No error, no log, no timeout.
7
+ //
8
+ // So the seam gives itself a deadline. Any byte of any kind re-arms it
9
+ // (reasoning models emit thinking deltas continuously, so silence really is
10
+ // silence). When it fires, the watchdog aborts ITS OWN controller and the
11
+ // caller's signal is only bridged in — which is what keeps a person's Stop
12
+ // distinguishable from our timeout. One is their cancel and is never retried;
13
+ // the other is ours, is transient, and fires while nothing has streamed yet,
14
+ // so the retry is always safe.
15
+ import { ProviderError } from "./errors.js";
16
+ /** No byte at all for this long and the stream is considered wedged. */
17
+ export const STREAM_IDLE_MS = 60_000;
18
+ export function streamWatch(opts = {}) {
19
+ const provider = opts.provider ?? "provider";
20
+ const idleMs = opts.idleMs ?? STREAM_IDLE_MS;
21
+ const callerSignal = opts.signal;
22
+ const started = Date.now();
23
+ const timeout = new AbortController();
24
+ let firstChunk = null;
25
+ let idle = false;
26
+ let disposed = false;
27
+ // The bridge is structural rather than an event listener: AbortSignal.any
28
+ // aborts synchronously when an input is ALREADY aborted, which is the race
29
+ // no listener can catch (the event fired before we subscribed).
30
+ const signal = callerSignal ? AbortSignal.any([callerSignal, timeout.signal]) : timeout.signal;
31
+ const idleError = (cause) => new ProviderError(provider, "timeout", `stream went ${idleMs / 1000}s without a byte`, {
32
+ cause,
33
+ });
34
+ function arm() {
35
+ const timer = setTimeout(() => {
36
+ idle = true;
37
+ timeout.abort(idleError());
38
+ }, idleMs);
39
+ // An orphaned watch — its consumer gone, dispose never called — must not
40
+ // hold a Node event loop open for a full deadline.
41
+ timer.unref?.();
42
+ return timer;
43
+ }
44
+ let timer = arm();
45
+ return {
46
+ signal,
47
+ sawByte() {
48
+ firstChunk ??= Date.now() - started;
49
+ clearTimeout(timer);
50
+ if (!disposed && !signal.aborted)
51
+ timer = arm();
52
+ },
53
+ firstChunkMs: () => firstChunk,
54
+ classify(err) {
55
+ // Our deadline, not theirs — and not the caller's Stop.
56
+ if (idle && !(callerSignal?.aborted ?? false))
57
+ return idleError(err);
58
+ return err;
59
+ },
60
+ dispose() {
61
+ disposed = true;
62
+ clearTimeout(timer);
63
+ },
64
+ };
65
+ }
66
+ /**
67
+ * Wrap a stream so every chunk re-arms `watch`, and a failure is re-classified
68
+ * through it. Disposes on any exit — completion, throw, or the consumer
69
+ * breaking out of the loop.
70
+ */
71
+ export async function* watchChunks(watch, chunks) {
72
+ try {
73
+ for await (const chunk of chunks) {
74
+ watch.sawByte();
75
+ yield chunk;
76
+ }
77
+ }
78
+ catch (err) {
79
+ throw watch.classify(err);
80
+ }
81
+ finally {
82
+ watch.dispose();
83
+ }
84
+ }
85
+ //# sourceMappingURL=watchdog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watchdog.js","sourceRoot":"","sources":["../src/watchdog.ts"],"names":[],"mappings":"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"}
package/dist/zod.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+ import { type Tool, type ToolContext } from "./tools.ts";
3
+ import type { JsonObjectSchema } from "./types.ts";
4
+ /** A zod schema as the JSON Schema every provider's tool contract wants. */
5
+ export declare function toJsonObjectSchema(schema: z.ZodType, label?: string): JsonObjectSchema;
6
+ export interface ZodToolSpec<I, O> {
7
+ name: string;
8
+ description: string;
9
+ input: z.ZodType<I>;
10
+ run: (input: I, ctx: ToolContext) => Promise<O>;
11
+ summarize?: (output: O) => string;
12
+ timeoutMs?: number;
13
+ isReadOnly?: boolean;
14
+ needsApproval?: boolean;
15
+ isConcurrencySafe?: boolean;
16
+ isTerminal?: boolean;
17
+ /**
18
+ * Clamp overflow to the bounds the schema already advertised instead of
19
+ * rejecting the call. Worth it for a TERMINAL tool, which gets no second
20
+ * chance: a forced-submit salvage turn runs exactly once, and discarding an
21
+ * otherwise-valid answer over a few extra characters loses the whole run.
22
+ * Off by default — an ordinary tool can simply be called again.
23
+ */
24
+ clampOverflow?: boolean;
25
+ }
26
+ /**
27
+ * A tool whose arguments are validated by zod, with the failure reported to
28
+ * the MODEL in words it can act on — `topic: expected string, received number`
29
+ * beats a stack trace it cannot read.
30
+ */
31
+ export declare function zodTool<I, O>(spec: ZodToolSpec<I, O>): Tool<I, O>;
32
+ //# sourceMappingURL=zod.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zod.d.ts","sourceRoot":"","sources":["../src/zod.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAc,KAAK,IAAI,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAErE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE,KAAK,SAAW,GAAG,gBAAgB,CASxF;AAED,MAAM,WAAW,WAAW,CAAC,CAAC,EAAE,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACpB,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAChD,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAyBjE"}
package/dist/zod.js ADDED
@@ -0,0 +1,49 @@
1
+ // The optional zod ergonomics — `@providerkit/core/zod`.
2
+ //
3
+ // Kept behind its own entry point so the core stays dependency-free for
4
+ // consumers that have no zod (a browser extension counting every byte, for
5
+ // one). Import this and tools become typed end to end.
6
+ import { z } from "zod";
7
+ import { defineTool } from "./tools.js";
8
+ import { clampToSchema } from "./schema.js";
9
+ /** A zod schema as the JSON Schema every provider's tool contract wants. */
10
+ export function toJsonObjectSchema(schema, label = "schema") {
11
+ const json = z.toJSONSchema(schema, { io: "input" });
12
+ if (json.type !== "object") {
13
+ // Every provider requires an object at the top level of a tool's
14
+ // parameters; a bare string or array is rejected at the wire, far from
15
+ // here, with a message that names none of this.
16
+ throw new Error(`providerkit: ${label} must be an object schema, got ${String(json.type)}`);
17
+ }
18
+ return json;
19
+ }
20
+ /**
21
+ * A tool whose arguments are validated by zod, with the failure reported to
22
+ * the MODEL in words it can act on — `topic: expected string, received number`
23
+ * beats a stack trace it cannot read.
24
+ */
25
+ export function zodTool(spec) {
26
+ const inputSchema = toJsonObjectSchema(spec.input, `Tool "${spec.name}" input`);
27
+ return defineTool({
28
+ name: spec.name,
29
+ description: spec.description,
30
+ inputSchema,
31
+ ...(spec.summarize ? { summarize: spec.summarize } : {}),
32
+ ...(spec.timeoutMs !== undefined ? { timeoutMs: spec.timeoutMs } : {}),
33
+ ...(spec.isReadOnly !== undefined ? { isReadOnly: spec.isReadOnly } : {}),
34
+ ...(spec.needsApproval !== undefined ? { needsApproval: spec.needsApproval } : {}),
35
+ ...(spec.isConcurrencySafe !== undefined ? { isConcurrencySafe: spec.isConcurrencySafe } : {}),
36
+ ...(spec.isTerminal !== undefined ? { isTerminal: spec.isTerminal } : {}),
37
+ validate: (raw) => {
38
+ const candidate = spec.clampOverflow ? clampToSchema(raw, inputSchema) : raw;
39
+ const parsed = spec.input.safeParse(candidate);
40
+ if (parsed.success)
41
+ return parsed.data;
42
+ throw new Error(parsed.error.issues
43
+ .map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`)
44
+ .join("; "));
45
+ },
46
+ run: spec.run,
47
+ });
48
+ }
49
+ //# sourceMappingURL=zod.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zod.js","sourceRoot":"","sources":["../src/zod.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@providerkit/core",
3
+ "version": "0.1.0",
4
+ "description": "The layer under your agent loop: one seam for every LLM provider, plus the failure handling you only learn in production.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Gustavo Salom\u00e9",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./zod": {
14
+ "types": "./dist/zod.d.ts",
15
+ "default": "./dist/zod.js"
16
+ }
17
+ },
18
+ "types": "./dist/index.d.ts",
19
+ "files": [
20
+ "dist",
21
+ "src",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "sideEffects": false,
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "keywords": [
30
+ "llm",
31
+ "openai",
32
+ "anthropic",
33
+ "gemini",
34
+ "agent",
35
+ "streaming",
36
+ "sse",
37
+ "retry",
38
+ "tool-calling"
39
+ ],
40
+ "scripts": {
41
+ "build": "tsc",
42
+ "typecheck": "tsc --noEmit",
43
+ "test": "vitest run",
44
+ "test:watch": "vitest",
45
+ "lint": "eslint src test",
46
+ "format": "prettier --write .",
47
+ "sync:docs": "cp ../README.md ../LICENSE .",
48
+ "prepublishOnly": "bun run lint && bun run typecheck && bun run test && bun run build && bun run sync:docs",
49
+ "release": "bun publish --access public",
50
+ "release:patch": "bun pm version patch && bun publish --access public",
51
+ "release:minor": "bun pm version minor && bun publish --access public",
52
+ "release:major": "bun pm version major && bun publish --access public",
53
+ "release:alpha": "bun publish --access public --tag alpha"
54
+ },
55
+ "peerDependencies": {
56
+ "zod": "^4"
57
+ },
58
+ "peerDependenciesMeta": {
59
+ "zod": {
60
+ "optional": true
61
+ }
62
+ },
63
+ "devDependencies": {
64
+ "typescript": "^5.9.3",
65
+ "vitest": "^4.1.2",
66
+ "zod": "^4.3.6"
67
+ },
68
+ "repository": {
69
+ "type": "git",
70
+ "url": "git+https://github.com/providerkit/providerkit.git"
71
+ },
72
+ "homepage": "https://providerkit.dev",
73
+ "bugs": {
74
+ "url": "https://github.com/providerkit/providerkit/issues"
75
+ }
76
+ }
package/src/context.ts ADDED
@@ -0,0 +1,150 @@
1
+ // Deciding when a conversation no longer fits, and where to cut it.
2
+ //
3
+ // Everything here is pure and testable. The model call that writes the summary
4
+ // and the row that stores it belong to the caller — what is decidable without
5
+ // I/O is decided here.
6
+ import type { ChatMessage } from "./types.ts";
7
+
8
+ /**
9
+ * What the answer needs after the prompt: an output budget, plus the tools'
10
+ * own schemas, plus the slack no provider documents.
11
+ *
12
+ * Absolute rather than a percentage on purpose — a 1M window does not need a
13
+ * 100k cushion, and a 128k window needs more than 12k.
14
+ */
15
+ export const CONTEXT_RESERVE_TOKENS = 32_000;
16
+
17
+ /**
18
+ * Four characters per token — the rule of thumb every provider's own
19
+ * calculator agrees with to within a fifth, which is all the precision this
20
+ * needs. It decides WHEN to fold: folding one turn early costs a cheap model
21
+ * call, folding one turn late costs the whole turn.
22
+ */
23
+ export function estimateTokens(text: string): number {
24
+ return Math.ceil(text.length / 4);
25
+ }
26
+
27
+ function textOf(message: ChatMessage): string {
28
+ if (message.role === "user" && typeof message.content !== "string") {
29
+ // Only the words are countable; an image's cost is the provider's own
30
+ // arithmetic and no character count approximates it.
31
+ return message.content.map((part) => (part.type === "text" ? part.text : "")).join("");
32
+ }
33
+ return typeof message.content === "string" ? message.content : "";
34
+ }
35
+
36
+ /** What one message costs to re-send: its words, its reasoning, and the
37
+ * arguments of any calls it made. */
38
+ export function messageTokens(message: ChatMessage): number {
39
+ const extra =
40
+ message.role === "assistant"
41
+ ? (message.reasoning ?? "") +
42
+ (message.toolCalls?.map((call) => call.name + call.arguments).join("") ?? "")
43
+ : "";
44
+ return estimateTokens(textOf(message) + extra);
45
+ }
46
+
47
+ export function conversationTokens(messages: readonly ChatMessage[]): number {
48
+ return messages.reduce((total, message) => total + messageTokens(message), 0);
49
+ }
50
+
51
+ /**
52
+ * The provider's own input count says the wall is close — fold before the next
53
+ * step rather than after the 400.
54
+ *
55
+ * Takes the REPORTED count, not an estimate, because it is the only figure
56
+ * that is not a guess. Estimate only when there is no reported count yet.
57
+ */
58
+ export function needsCompaction(inputTokens: number, contextWindow: number): boolean {
59
+ return inputTokens >= contextWindow - CONTEXT_RESERVE_TOKENS;
60
+ }
61
+
62
+ /**
63
+ * How much of the window the history BEHIND the current turn may spend: a
64
+ * tenth, floored so a small window still gets a usable memory. The tail stays
65
+ * verbatim and the summary is short, so the rest is what folding buys back.
66
+ */
67
+ export function historyBudgetTokens(contextWindow: number): number {
68
+ return Math.max(6_000, Math.floor(contextWindow * 0.1));
69
+ }
70
+
71
+ /**
72
+ * Where to cut so the messages AFTER the cut fit `budget`, walking backwards
73
+ * from the newest.
74
+ *
75
+ * Two invariants the cut must respect, and both are correctness rather than
76
+ * taste:
77
+ *
78
+ * 1. Never split a tool call from its result. Every provider rejects a tool
79
+ * result whose call is missing, so a cut landing between them produces a
80
+ * 400 on the very next turn — the failure compaction was called to avoid.
81
+ * 2. Never cut into the system prompt. It is not history.
82
+ *
83
+ * Returns the index the kept tail starts at, or 0 when everything already fits.
84
+ */
85
+ export function pickCut(messages: readonly ChatMessage[], budget: number): number {
86
+ const firstNonSystem = messages.findIndex((message) => message.role !== "system");
87
+ // Nothing but a system prompt: there is no history to fold.
88
+ if (firstNonSystem === -1) return messages.length;
89
+
90
+ // The newest message is always kept, budget or not. A conversation whose
91
+ // latest turn alone overruns the budget is not fixable by cutting history,
92
+ // and answering a summary instead of the question the person just asked is
93
+ // never the right failure.
94
+ const latest = messages.length - 1;
95
+ let cut = latest;
96
+ let total = messageTokens(messages[latest]!);
97
+
98
+ for (let i = latest - 1; i >= firstNonSystem; i--) {
99
+ total += messageTokens(messages[i]!);
100
+ if (total > budget) break;
101
+ cut = i;
102
+ }
103
+
104
+ // Walk back off any tool result whose assistant turn would be left behind
105
+ // it. Bounded by the first non-system message, so it can never run off the
106
+ // front. This can exceed the budget by a message or two, which is the right
107
+ // trade: an orphaned tool result is a hard 400, not an overrun.
108
+ while (cut > firstNonSystem && messages[cut]!.role === "tool") cut--;
109
+
110
+ return cut;
111
+ }
112
+
113
+ /**
114
+ * Fold `messages` into `[…system, summary, …tail]`.
115
+ *
116
+ * The summary arrives as a user turn rather than a system one: a system
117
+ * message added mid-conversation reads to the model as a new instruction, and
118
+ * several providers require the system block to be first and singular anyway.
119
+ */
120
+ export function applyCompaction(
121
+ messages: readonly ChatMessage[],
122
+ cut: number,
123
+ summary: string,
124
+ ): ChatMessage[] {
125
+ const system = messages.filter((message) => message.role === "system");
126
+ const tail = messages.slice(cut).filter((message) => message.role !== "system");
127
+ return [
128
+ ...system,
129
+ { role: "user", content: `[Earlier conversation, summarized]\n\n${summary}` },
130
+ ...tail,
131
+ ];
132
+ }
133
+
134
+ /**
135
+ * The context window for a model, when nothing volunteered one.
136
+ *
137
+ * The ladder exists because the endpoints disagree about whether to tell you:
138
+ * OpenRouter, LM Studio and Ollama publish `context_length`; Anthropic and
139
+ * OpenAI do not. A reported number always wins; this is the floor under it.
140
+ *
141
+ * A conservative default is the right failure: too small folds one turn early
142
+ * and costs a cheap call, too large hits a hard 400 mid-run.
143
+ */
144
+ export function guessContextWindow(model: string): number {
145
+ const id = model.toLowerCase();
146
+ if (/gemini|gpt-4\.1|grok-4|llama-4/.test(id)) return 1_000_000;
147
+ if (/claude|gpt-5|o[34]|deepseek|kimi|glm|qwen/.test(id)) return 200_000;
148
+ if (/gpt-4o|mistral|command-r/.test(id)) return 128_000;
149
+ return 128_000;
150
+ }