@cana-ai/sdk 0.2.0 → 0.2.1
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/README.md +165 -0
- package/package.json +19 -3
package/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# @cana-ai/sdk
|
|
2
|
+
|
|
3
|
+
OpenAI-compatible client for Cana **Apps** — programmatic access to your agent
|
|
4
|
+
with **HMAC auth**, **server-hosted MCP** federation, **streaming**, and
|
|
5
|
+
**typed structured output**.
|
|
6
|
+
|
|
7
|
+
Point any OpenAI SDK at a Cana App and it just works. Or use this client to get
|
|
8
|
+
auto MCP dispatch, Web-Crypto signing, and Zod typing for free.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @cana-ai/sdk
|
|
12
|
+
# or just drop a script tag — see below
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Quick start (Node / Bun / Deno / Workers)
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { CanaClient } from "@cana-ai/sdk/openai";
|
|
21
|
+
|
|
22
|
+
const client = new CanaClient({
|
|
23
|
+
appId: "app_…",
|
|
24
|
+
signingKey: process.env.CANA_APP_KEY!, // csk_live_…
|
|
25
|
+
apiBase: "https://cana.build", // optional
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const c = await client.chat.completions.create({
|
|
29
|
+
model: "openai/gpt-4o",
|
|
30
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
31
|
+
});
|
|
32
|
+
console.log(c.choices[0].message.content);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Model strings are `<provider>/<model-id>` — `openai/gpt-4o`,
|
|
36
|
+
`anthropic/claude-haiku-4-5-20251001`, `moonshot/moonshot-v1-32k`,
|
|
37
|
+
`google/gemini-2.5-pro`, etc. The proxy resolves the right credentials from the
|
|
38
|
+
App owner's provider configs.
|
|
39
|
+
|
|
40
|
+
## Streaming
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const stream = await client.chat.completions.create({
|
|
44
|
+
model: "openai/gpt-4o",
|
|
45
|
+
messages: [{ role: "user", content: "Write a haiku about TCP." }],
|
|
46
|
+
stream: true,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
for await (const chunk of stream) {
|
|
50
|
+
process.stdout.write(chunk.choices[0]?.delta.content ?? "");
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Chunks are verbatim OpenAI Server-Sent-Events shapes, with `[DONE]`
|
|
55
|
+
auto-terminated.
|
|
56
|
+
|
|
57
|
+
## Typed structured output (Zod)
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { z } from "zod";
|
|
61
|
+
|
|
62
|
+
const Person = z.object({ name: z.string(), age: z.number() });
|
|
63
|
+
|
|
64
|
+
const { result } = await client.chat.completions.generate({
|
|
65
|
+
model: "openai/gpt-4o",
|
|
66
|
+
messages: [{ role: "user", content: "Pick a friendly person" }],
|
|
67
|
+
schema: Person,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
result.name; // string
|
|
71
|
+
result.age; // number
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Uses OpenAI's strict `response_format: { type: "json_schema" }` under the hood.
|
|
75
|
+
Optional peer dep: `zod-to-json-schema` (auto-converts your schema).
|
|
76
|
+
|
|
77
|
+
## MCP federation — discover + auto-dispatch
|
|
78
|
+
|
|
79
|
+
Server-hosted MCP connectors are exposed via two helper endpoints; the SDK
|
|
80
|
+
drives the loop for you:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
const r = await client.runWithMcp({
|
|
84
|
+
model: "anthropic/claude-haiku-4-5-20251001",
|
|
85
|
+
messages: [{ role: "user", content: "Find last week's PRs in cana-web" }],
|
|
86
|
+
mcp: ["github", "search"], // connector names attached to your App
|
|
87
|
+
maxRounds: 5,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
console.log(r.message.content);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Or do it by hand:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const { tools } = await client.mcp.tools(["github"]);
|
|
97
|
+
// `tools` is in OpenAI tool format → pass to chat.completions.create
|
|
98
|
+
// When you get a tool_call back named `mcp__github__<tool>`, dispatch:
|
|
99
|
+
const out = await client.mcp.execute({ name: "mcp__github__list_prs", arguments: { repo: "…" } });
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Tool names follow the `mcp__<connector>__<tool>` convention; everything else
|
|
103
|
+
is treated as a client-side tool you handle yourself.
|
|
104
|
+
|
|
105
|
+
## Browser — `<script>` tag
|
|
106
|
+
|
|
107
|
+
A pre-built IIFE bundle is hosted at the App owner's Cana URL:
|
|
108
|
+
|
|
109
|
+
```html
|
|
110
|
+
<script src="https://cana.build/sdk/openai.js"></script>
|
|
111
|
+
<script>
|
|
112
|
+
const client = new CanaOpenAI.CanaClient({
|
|
113
|
+
appId: "app_…",
|
|
114
|
+
signingKey: "csk_live_…", // server-issued, scoped to one App
|
|
115
|
+
});
|
|
116
|
+
client.chat.completions.create({
|
|
117
|
+
model: "openai/gpt-4o",
|
|
118
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
119
|
+
}).then(c => console.log(c.choices[0].message.content));
|
|
120
|
+
</script>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
> Browser-direct mode leaks the signing key to anyone who views source. Use it
|
|
124
|
+
> for prototypes; in prod, proxy through your own server.
|
|
125
|
+
|
|
126
|
+
## Plain OpenAI SDK
|
|
127
|
+
|
|
128
|
+
Any OpenAI client can hit the proxy directly if it can sign requests. The
|
|
129
|
+
URL shape is `https://<base>/api/v1/apps/<appId>/openai`. You'll need a fetch
|
|
130
|
+
hook that adds:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
X-Cana-App-Signature: t=<unix-ms>,v1=<hmac-sha256-hex>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
over the canonical string `${unixMs}.${rawBody}`. The Node/browser examples
|
|
137
|
+
above do this for you.
|
|
138
|
+
|
|
139
|
+
## Errors
|
|
140
|
+
|
|
141
|
+
All non-2xx responses throw `CanaApiError` with a typed `.code` and `.status`.
|
|
142
|
+
Common codes: `signature_invalid`, `signature_expired`, `app_paused`,
|
|
143
|
+
`rate_limited`, `billing_exceeded`, `unknown_provider`, `model_not_found`,
|
|
144
|
+
`mcp_connector_unknown`, `upstream_error`.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { CanaApiError } from "@cana-ai/sdk/openai";
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
await client.chat.completions.create({ … });
|
|
151
|
+
} catch (e) {
|
|
152
|
+
if (e instanceof CanaApiError && e.code === "rate_limited") {
|
|
153
|
+
// back off, check `Retry-After`
|
|
154
|
+
} else throw e;
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Web Crypto only
|
|
159
|
+
|
|
160
|
+
Zero `node:crypto` import — works on Node 18+, Bun, Deno, Cloudflare Workers,
|
|
161
|
+
and modern browsers out of the box.
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cana-ai/sdk",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Cana Apps
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "OpenAI-compatible client for Cana Apps — HMAC auth, server-hosted MCP federation, streaming, typed structured output. Web Crypto only (Node 18+, Bun, Deno, Workers, browsers).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -16,7 +16,23 @@
|
|
|
16
16
|
"import": "./dist/openai/index.js"
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
|
-
"files": ["dist"],
|
|
19
|
+
"files": ["dist", "README.md"],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"cana",
|
|
22
|
+
"openai",
|
|
23
|
+
"openai-compatible",
|
|
24
|
+
"llm",
|
|
25
|
+
"agent",
|
|
26
|
+
"mcp",
|
|
27
|
+
"model-context-protocol",
|
|
28
|
+
"structured-output",
|
|
29
|
+
"hmac",
|
|
30
|
+
"ai-sdk",
|
|
31
|
+
"anthropic",
|
|
32
|
+
"moonshot"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"homepage": "https://cana.build",
|
|
20
36
|
"scripts": {
|
|
21
37
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
22
38
|
"dev": "tsc -p tsconfig.json --watch",
|