@canarycoders/ai 0.3.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/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/compat.cjs +17 -0
- package/dist/compat.cjs.map +1 -0
- package/dist/compat.d.cts +19 -0
- package/dist/compat.d.ts +19 -0
- package/dist/compat.js +14 -0
- package/dist/compat.js.map +1 -0
- package/dist/index.cjs +1658 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1070 -0
- package/dist/index.d.ts +1068 -0
- package/dist/index.js +1638 -0
- package/dist/index.js.map +1 -0
- package/dist/realtime-BWDkXcj9.d.cts +61 -0
- package/dist/realtime-BWDkXcj9.d.ts +61 -0
- package/dist/realtime-client/index.cjs +82 -0
- package/dist/realtime-client/index.cjs.map +1 -0
- package/dist/realtime-client/index.d.cts +30 -0
- package/dist/realtime-client/index.d.ts +30 -0
- package/dist/realtime-client/index.js +80 -0
- package/dist/realtime-client/index.js.map +1 -0
- package/package.json +81 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CanaryCoders
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# @canarycoders/ai
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for the [CanaryCoders AI](https://ai.canarycoders.es) gateway. Runs on Node 18+ and Bun, with no runtime dependencies.
|
|
4
|
+
|
|
5
|
+
**Docs:** https://docs.ai.canarycoders.es
|
|
6
|
+
|
|
7
|
+
One client covers chat, image/video/audio generation, embeddings, vision, conversational agents, realtime sessions, and usage data. The queue polling, SSE streaming, retries, and error typing are handled for you, so most calls are a single `await`.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun add @canarycoders/ai
|
|
13
|
+
# or: npm install @canarycoders/ai
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import CanaryLLM from "@canarycoders/ai";
|
|
20
|
+
|
|
21
|
+
const client = new CanaryLLM({ apiKey: process.env.CANARY_AI_API_KEY });
|
|
22
|
+
|
|
23
|
+
const res = await client.chat.complete({
|
|
24
|
+
provider: "openai",
|
|
25
|
+
model: "gpt-4o-mini",
|
|
26
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
console.log(res.content, res.usage.totalTokens);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`apiKey` defaults to `CANARY_AI_API_KEY` (legacy `CANARYLLM_API_KEY` still works) and `baseURL` to the hosted gateway, so `new CanaryLLM()` with the env var set is enough.
|
|
33
|
+
|
|
34
|
+
## How the queue works
|
|
35
|
+
|
|
36
|
+
The gateway runs most work through a queue: a request returns a `queueId`, and you poll for the result. The SDK does that for you. `chat.complete`, `images.generate`, `audio.speech`, and the rest submit the job and poll until it finishes, then resolve the typed result.
|
|
37
|
+
|
|
38
|
+
When you want the handle instead of waiting inline, every queued method has a `*Job` (or `submit`) sibling:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const job = await client.chat.submit({ provider: "openai", messages });
|
|
42
|
+
console.log(job.id); // queueId, e.g. to track out of band
|
|
43
|
+
const result = await job.result();
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Cancelling the await also cancels the job on the server:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const ctrl = new AbortController();
|
|
50
|
+
const pending = client.video.generate({ provider: "gemini", prompt }, { signal: ctrl.signal });
|
|
51
|
+
ctrl.abort(); // stops polling and fires queue/cancel
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Poll timing is tunable per call (`initialIntervalMs`, `maxIntervalMs`, `maxWaitMs`) or once on the client via the `poll` option. The default wait budget per operation matches the gateway's own timeouts.
|
|
55
|
+
|
|
56
|
+
## Streaming
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
for await (const event of client.chat.stream({
|
|
60
|
+
provider: "anthropic",
|
|
61
|
+
model: "claude-sonnet-4-5",
|
|
62
|
+
messages: [{ role: "user", content: "Write a haiku" }],
|
|
63
|
+
})) {
|
|
64
|
+
if (event.type === "text") process.stdout.write(event.delta);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Events are normalized regardless of the underlying provider: `start`, `text`, `thinking`, `tool_call`, `usage`, `done`. Anything the SDK doesn't recognize comes through as `{ type: "raw" }` rather than being dropped. If the gateway sends an error mid-stream, it throws out of the loop as a typed error.
|
|
69
|
+
|
|
70
|
+
## Media and vision
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const img = await client.images.generate({ provider: "openai", prompt: "a yellow canary" });
|
|
74
|
+
const speech = await client.audio.speech({ provider: "elevenlabs", text: "Hello there" });
|
|
75
|
+
const { text } = await client.audio.transcribe({ provider: "elevenlabs", audio, mimeType: "audio/mp3" });
|
|
76
|
+
const song = await client.audio.music({ prompt: "calm lo-fi", durationMs: 20000 });
|
|
77
|
+
const { detections } = await client.vision.detect({ image });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Embeddings
|
|
81
|
+
|
|
82
|
+
Embed text into vectors with a local model (LM Studio), for customer-side RAG. The gateway processes the text transiently and stores nothing — you keep the vectors and documents in your own store (e.g. pgvector, sqlite-vec).
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const { embeddings, dimensions } = await client.embeddings.create({
|
|
86
|
+
provider: "lmstudio",
|
|
87
|
+
model: "nomic-embed-text-v1.5",
|
|
88
|
+
input: ["first chunk of text", "second chunk of text"],
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
OpenAI embedding clients work too: point them at `<baseURL>/v1` and use `lmstudio/<model>`.
|
|
93
|
+
|
|
94
|
+
## Errors
|
|
95
|
+
|
|
96
|
+
Every failure is a subclass of `APIError`: `AuthenticationError`, `PermissionError`, `RateLimitError`, `BadRequestError`, `NotFoundError`, `InternalServerError`, and `APIConnectionError` / `APIConnectionTimeoutError`. Branch on `.code` or `.status`, not `.message`, since the gateway sanitizes messages in production.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { RateLimitError } from "@canarycoders/ai";
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
await client.chat.complete({ provider: "openai", messages });
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (err instanceof RateLimitError) {
|
|
105
|
+
console.warn("slow down", err.retryAfterMs, err.remaining);
|
|
106
|
+
} else {
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Transient failures (429, 5xx, network drops) retry with exponential backoff and jitter. The default is two retries; set `maxRetries` to change it. Queue submits never retry on 5xx, so a job can't be enqueued twice.
|
|
113
|
+
|
|
114
|
+
## Realtime and conversational agents
|
|
115
|
+
|
|
116
|
+
The SDK belongs on your backend, where the API key stays. It mints a short-lived credential; the browser opens the actual WebRTC connection.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { toBrokeredCredential } from "@canarycoders/ai";
|
|
120
|
+
|
|
121
|
+
// backend
|
|
122
|
+
const session = await client.realtime.sessions.create({ kind: "voice", voice: "alloy" });
|
|
123
|
+
return toBrokeredCredential(session); // safe to send to the browser
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
// browser
|
|
128
|
+
import { connectRealtime } from "@canarycoders/ai/realtime-client";
|
|
129
|
+
|
|
130
|
+
const conn = await connectRealtime({ credential, onEvent: console.log });
|
|
131
|
+
conn.send({ type: "response.create" });
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
For ElevenLabs conversational agents, `client.conversations.sessions.create()` returns a signed URL you hand to the ElevenLabs client.
|
|
135
|
+
|
|
136
|
+
## Drop-in OpenAI / Anthropic
|
|
137
|
+
|
|
138
|
+
Already using the official SDKs? Point them at the gateway and keep your code:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import OpenAI from "openai";
|
|
142
|
+
|
|
143
|
+
const oa = new OpenAI(client.compat.openai());
|
|
144
|
+
await oa.chat.completions.create({
|
|
145
|
+
model: "openai/gpt-4o-mini", // provider/modelId
|
|
146
|
+
messages,
|
|
147
|
+
stream: true,
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Client options
|
|
152
|
+
|
|
153
|
+
| Option | Default | Notes |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| `apiKey` | `CANARY_AI_API_KEY` | API key (`clk_live_…`) |
|
|
156
|
+
| `baseURL` | hosted gateway | override for self-hosted |
|
|
157
|
+
| `authStyle` | `"bearer"` | or `"x-api-key"` |
|
|
158
|
+
| `timeoutMs` | `60000` | per-request total timeout |
|
|
159
|
+
| `maxRetries` | `2` | retries for transient failures |
|
|
160
|
+
| `fetch` | global | inject a custom fetch |
|
|
161
|
+
| `defaultTag` | — | attached to requests for usage attribution |
|
|
162
|
+
| `poll` | — | default poll timing for queued ops |
|
|
163
|
+
|
|
164
|
+
## Keeping types in sync
|
|
165
|
+
|
|
166
|
+
The gateway API is the source of truth. Regenerate types from its OpenAPI spec and diff them against the hand-written ones:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
bun run gen:openapi
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
MIT
|
package/dist/compat.cjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/compat.ts
|
|
4
|
+
function v1(baseURL) {
|
|
5
|
+
return `${baseURL.replace(/\/+$/, "")}/v1`;
|
|
6
|
+
}
|
|
7
|
+
function openaiTarget(baseURL, apiKey) {
|
|
8
|
+
return { baseURL: v1(baseURL), apiKey };
|
|
9
|
+
}
|
|
10
|
+
function anthropicTarget(baseURL, apiKey) {
|
|
11
|
+
return { baseURL: v1(baseURL), apiKey };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
exports.anthropicTarget = anthropicTarget;
|
|
15
|
+
exports.openaiTarget = openaiTarget;
|
|
16
|
+
//# sourceMappingURL=compat.cjs.map
|
|
17
|
+
//# sourceMappingURL=compat.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/compat.ts"],"names":[],"mappings":";;;AAgBA,SAAS,GAAG,OAAA,EAAyB;AACnC,EAAA,OAAO,CAAA,EAAG,OAAA,CAAQ,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAC,CAAA,GAAA,CAAA;AACvC;AAEO,SAAS,YAAA,CAAa,SAAiB,MAAA,EAA8B;AAC1E,EAAA,OAAO,EAAE,OAAA,EAAS,EAAA,CAAG,OAAO,GAAG,MAAA,EAAO;AACxC;AAEO,SAAS,eAAA,CAAgB,SAAiB,MAAA,EAA8B;AAC7E,EAAA,OAAO,EAAE,OAAA,EAAS,EAAA,CAAG,OAAO,GAAG,MAAA,EAAO;AACxC","file":"compat.cjs","sourcesContent":["/**\n * Helpers to point the official OpenAI / Anthropic SDKs at CanaryLLM's\n * drop-in compatible endpoints. Use the `provider/modelId` model-string format\n * (e.g. `\"openai/gpt-4o-mini\"`, `\"anthropic/claude-sonnet-4-5\"`).\n *\n * @example\n * import OpenAI from \"openai\";\n * import { openaiTarget } from \"@canarycoders/ai/compat\";\n * const oa = new OpenAI(openaiTarget(baseURL, apiKey));\n */\nexport interface CompatTarget {\n baseURL: string;\n apiKey: string;\n defaultHeaders?: Record<string, string>;\n}\n\nfunction v1(baseURL: string): string {\n return `${baseURL.replace(/\\/+$/, \"\")}/v1`;\n}\n\nexport function openaiTarget(baseURL: string, apiKey: string): CompatTarget {\n return { baseURL: v1(baseURL), apiKey };\n}\n\nexport function anthropicTarget(baseURL: string, apiKey: string): CompatTarget {\n return { baseURL: v1(baseURL), apiKey };\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers to point the official OpenAI / Anthropic SDKs at CanaryLLM's
|
|
3
|
+
* drop-in compatible endpoints. Use the `provider/modelId` model-string format
|
|
4
|
+
* (e.g. `"openai/gpt-4o-mini"`, `"anthropic/claude-sonnet-4-5"`).
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import OpenAI from "openai";
|
|
8
|
+
* import { openaiTarget } from "@canarycoders/ai/compat";
|
|
9
|
+
* const oa = new OpenAI(openaiTarget(baseURL, apiKey));
|
|
10
|
+
*/
|
|
11
|
+
interface CompatTarget {
|
|
12
|
+
baseURL: string;
|
|
13
|
+
apiKey: string;
|
|
14
|
+
defaultHeaders?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
declare function openaiTarget(baseURL: string, apiKey: string): CompatTarget;
|
|
17
|
+
declare function anthropicTarget(baseURL: string, apiKey: string): CompatTarget;
|
|
18
|
+
|
|
19
|
+
export { type CompatTarget, anthropicTarget, openaiTarget };
|
package/dist/compat.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers to point the official OpenAI / Anthropic SDKs at CanaryLLM's
|
|
3
|
+
* drop-in compatible endpoints. Use the `provider/modelId` model-string format
|
|
4
|
+
* (e.g. `"openai/gpt-4o-mini"`, `"anthropic/claude-sonnet-4-5"`).
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import OpenAI from "openai";
|
|
8
|
+
* import { openaiTarget } from "@canarycoders/ai/compat";
|
|
9
|
+
* const oa = new OpenAI(openaiTarget(baseURL, apiKey));
|
|
10
|
+
*/
|
|
11
|
+
interface CompatTarget {
|
|
12
|
+
baseURL: string;
|
|
13
|
+
apiKey: string;
|
|
14
|
+
defaultHeaders?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
declare function openaiTarget(baseURL: string, apiKey: string): CompatTarget;
|
|
17
|
+
declare function anthropicTarget(baseURL: string, apiKey: string): CompatTarget;
|
|
18
|
+
|
|
19
|
+
export { type CompatTarget, anthropicTarget, openaiTarget };
|
package/dist/compat.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/compat.ts
|
|
2
|
+
function v1(baseURL) {
|
|
3
|
+
return `${baseURL.replace(/\/+$/, "")}/v1`;
|
|
4
|
+
}
|
|
5
|
+
function openaiTarget(baseURL, apiKey) {
|
|
6
|
+
return { baseURL: v1(baseURL), apiKey };
|
|
7
|
+
}
|
|
8
|
+
function anthropicTarget(baseURL, apiKey) {
|
|
9
|
+
return { baseURL: v1(baseURL), apiKey };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export { anthropicTarget, openaiTarget };
|
|
13
|
+
//# sourceMappingURL=compat.js.map
|
|
14
|
+
//# sourceMappingURL=compat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/compat.ts"],"names":[],"mappings":";AAgBA,SAAS,GAAG,OAAA,EAAyB;AACnC,EAAA,OAAO,CAAA,EAAG,OAAA,CAAQ,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAC,CAAA,GAAA,CAAA;AACvC;AAEO,SAAS,YAAA,CAAa,SAAiB,MAAA,EAA8B;AAC1E,EAAA,OAAO,EAAE,OAAA,EAAS,EAAA,CAAG,OAAO,GAAG,MAAA,EAAO;AACxC;AAEO,SAAS,eAAA,CAAgB,SAAiB,MAAA,EAA8B;AAC7E,EAAA,OAAO,EAAE,OAAA,EAAS,EAAA,CAAG,OAAO,GAAG,MAAA,EAAO;AACxC","file":"compat.js","sourcesContent":["/**\n * Helpers to point the official OpenAI / Anthropic SDKs at CanaryLLM's\n * drop-in compatible endpoints. Use the `provider/modelId` model-string format\n * (e.g. `\"openai/gpt-4o-mini\"`, `\"anthropic/claude-sonnet-4-5\"`).\n *\n * @example\n * import OpenAI from \"openai\";\n * import { openaiTarget } from \"@canarycoders/ai/compat\";\n * const oa = new OpenAI(openaiTarget(baseURL, apiKey));\n */\nexport interface CompatTarget {\n baseURL: string;\n apiKey: string;\n defaultHeaders?: Record<string, string>;\n}\n\nfunction v1(baseURL: string): string {\n return `${baseURL.replace(/\\/+$/, \"\")}/v1`;\n}\n\nexport function openaiTarget(baseURL: string, apiKey: string): CompatTarget {\n return { baseURL: v1(baseURL), apiKey };\n}\n\nexport function anthropicTarget(baseURL: string, apiKey: string): CompatTarget {\n return { baseURL: v1(baseURL), apiKey };\n}\n"]}
|