@caether/sdk 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.
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/package.json +43 -0
- package/src/chat/chat.js +139 -0
- package/src/chat/content.js +41 -0
- package/src/chat/index.d.ts +93 -0
- package/src/chat/index.js +17 -0
- package/src/chat/types.js +113 -0
- package/src/client.js +48 -0
- package/src/constants.js +8 -0
- package/src/errors.js +82 -0
- package/src/index.d.ts +47 -0
- package/src/index.js +17 -0
- package/src/transport.js +167 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CaetherAI
|
|
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,61 @@
|
|
|
1
|
+
# @caether/sdk
|
|
2
|
+
|
|
3
|
+
Official JavaScript/Node.js SDK for the [CaetherAI](https://caether.ai) API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @caether/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Node.js 18+ (uses the global `fetch`).
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { Client } from "@caether/sdk";
|
|
17
|
+
import { user, system } from "@caether/sdk/chat";
|
|
18
|
+
|
|
19
|
+
const client = new Client({ apiKey: process.env.CAETHER_API_KEY });
|
|
20
|
+
|
|
21
|
+
const chat = client.chat.create({ model: "caether-1.1" });
|
|
22
|
+
chat.append(system("You are Caether, an AI agent built to answer helpful questions."));
|
|
23
|
+
chat.append(user("How big is the universe?"));
|
|
24
|
+
|
|
25
|
+
const response = await chat.sample();
|
|
26
|
+
console.log(String(response)); // text content
|
|
27
|
+
console.log(response.id); // response ID to continue later
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Streaming
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
const chat = client.chat.create({ model: "caether-1.1" });
|
|
34
|
+
chat.append(user("Write a haiku about the sea."));
|
|
35
|
+
|
|
36
|
+
const { response, stream } = chat.stream();
|
|
37
|
+
for await (const chunk of stream) {
|
|
38
|
+
process.stdout.write(chunk.content);
|
|
39
|
+
}
|
|
40
|
+
console.log("\n", response.content);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Chaining the conversation
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
const chat2 = client.chat.create({
|
|
47
|
+
model: "caether-1.1",
|
|
48
|
+
previousResponseId: response.id,
|
|
49
|
+
});
|
|
50
|
+
chat2.append(user("How do stars form?"));
|
|
51
|
+
const second = await chat2.sample();
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Retrieve / delete a stored response
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
const stored = await client.chat.getStoredCompletion("<response id>");
|
|
58
|
+
await client.chat.deleteStoredCompletion("<response id>");
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
See [docs.caether.ai](https://docs.caether.ai) for full documentation.
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@caether/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official JavaScript/Node.js SDK for the CaetherAI API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "CaetherAI",
|
|
8
|
+
"homepage": "https://caether.ai",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"main": "./src/index.js",
|
|
13
|
+
"types": "./src/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./src/index.d.ts",
|
|
17
|
+
"import": "./src/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./chat": {
|
|
20
|
+
"types": "./src/chat/index.d.ts",
|
|
21
|
+
"import": "./src/chat/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"keywords": [
|
|
30
|
+
"caether",
|
|
31
|
+
"caetherai",
|
|
32
|
+
"ai",
|
|
33
|
+
"llm",
|
|
34
|
+
"sdk",
|
|
35
|
+
"api"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "node --test"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/chat/chat.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Response, ResponseChunk } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export class Chat {
|
|
4
|
+
constructor(transport, options) {
|
|
5
|
+
this._transport = transport;
|
|
6
|
+
this._path = options.path;
|
|
7
|
+
this.model = options.model;
|
|
8
|
+
this.storeMessages = options.storeMessages ?? true;
|
|
9
|
+
this.previousResponseId = options.previousResponseId ?? null;
|
|
10
|
+
this.useEncryptedContent = options.useEncryptedContent ?? false;
|
|
11
|
+
this.temperature = options.temperature;
|
|
12
|
+
this.topP = options.topP;
|
|
13
|
+
this.maxOutputTokens = options.maxOutputTokens;
|
|
14
|
+
this.reasoningEffort = options.reasoningEffort;
|
|
15
|
+
this.tools = options.tools;
|
|
16
|
+
this.toolChoice = options.toolChoice;
|
|
17
|
+
this.extraBody = options.extraBody || {};
|
|
18
|
+
this.messages = [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
append(message) {
|
|
22
|
+
if (message instanceof Response) {
|
|
23
|
+
this.previousResponseId = message.id || this.previousResponseId;
|
|
24
|
+
for (const item of message.output) this.messages.push(item);
|
|
25
|
+
} else {
|
|
26
|
+
this.messages.push(message);
|
|
27
|
+
}
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_buildBody(stream) {
|
|
32
|
+
const body = {
|
|
33
|
+
model: this.model,
|
|
34
|
+
messages: this.messages,
|
|
35
|
+
stream,
|
|
36
|
+
store: this.storeMessages,
|
|
37
|
+
};
|
|
38
|
+
if (this.previousResponseId !== null && this.previousResponseId !== undefined)
|
|
39
|
+
body.previous_response_id = this.previousResponseId;
|
|
40
|
+
if (this.temperature !== undefined) body.temperature = this.temperature;
|
|
41
|
+
if (this.topP !== undefined) body.top_p = this.topP;
|
|
42
|
+
if (this.maxOutputTokens !== undefined)
|
|
43
|
+
body.max_output_tokens = this.maxOutputTokens;
|
|
44
|
+
if (this.reasoningEffort !== undefined)
|
|
45
|
+
body.reasoning_effort = this.reasoningEffort;
|
|
46
|
+
if (this.tools !== undefined) body.tools = this.tools;
|
|
47
|
+
if (this.toolChoice !== undefined) body.tool_choice = this.toolChoice;
|
|
48
|
+
if (this.useEncryptedContent) body.include = ["reasoning.encrypted_content"];
|
|
49
|
+
Object.assign(body, this.extraBody);
|
|
50
|
+
return body;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async sample() {
|
|
54
|
+
const data = await this._transport.request("POST", this._path, {
|
|
55
|
+
body: this._buildBody(false),
|
|
56
|
+
});
|
|
57
|
+
const response = Response.fromObject(data);
|
|
58
|
+
if (response.id) this.previousResponseId = response.id;
|
|
59
|
+
return response;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
stream() {
|
|
63
|
+
const accumulator = new Response({ model: this.model, output: [] });
|
|
64
|
+
const events = this._transport.stream("POST", this._path, {
|
|
65
|
+
body: this._buildBody(true),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
async function* generator() {
|
|
69
|
+
const contentParts = [];
|
|
70
|
+
const reasoningParts = [];
|
|
71
|
+
for await (const event of events) {
|
|
72
|
+
const chunk = ResponseChunk.fromEvent(event);
|
|
73
|
+
if (chunk.content) {
|
|
74
|
+
contentParts.push(chunk.content);
|
|
75
|
+
accumulator.content = contentParts.join("");
|
|
76
|
+
}
|
|
77
|
+
if (chunk.reasoningContent) {
|
|
78
|
+
reasoningParts.push(chunk.reasoningContent);
|
|
79
|
+
accumulator.reasoningContent = reasoningParts.join("");
|
|
80
|
+
}
|
|
81
|
+
if (chunk.usage) accumulator.usage = chunk.usage;
|
|
82
|
+
if (chunk.functionCall) accumulator.functionCalls.push(chunk.functionCall);
|
|
83
|
+
yield chunk;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { response: accumulator, stream: generator() };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class ChatClient {
|
|
92
|
+
constructor(transport, { path = "/chat/completions" } = {}) {
|
|
93
|
+
this._transport = transport;
|
|
94
|
+
this._path = path;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
create(options = {}) {
|
|
98
|
+
const {
|
|
99
|
+
model,
|
|
100
|
+
storeMessages,
|
|
101
|
+
previousResponseId,
|
|
102
|
+
useEncryptedContent,
|
|
103
|
+
temperature,
|
|
104
|
+
topP,
|
|
105
|
+
maxOutputTokens,
|
|
106
|
+
reasoningEffort,
|
|
107
|
+
tools,
|
|
108
|
+
toolChoice,
|
|
109
|
+
...extraBody
|
|
110
|
+
} = options;
|
|
111
|
+
if (!model) throw new Error("model is required");
|
|
112
|
+
return new Chat(this._transport, {
|
|
113
|
+
path: this._path,
|
|
114
|
+
model,
|
|
115
|
+
storeMessages,
|
|
116
|
+
previousResponseId,
|
|
117
|
+
useEncryptedContent,
|
|
118
|
+
temperature,
|
|
119
|
+
topP,
|
|
120
|
+
maxOutputTokens,
|
|
121
|
+
reasoningEffort,
|
|
122
|
+
tools,
|
|
123
|
+
toolChoice,
|
|
124
|
+
extraBody,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async getStoredCompletion(responseId) {
|
|
129
|
+
const data = await this._transport.request(
|
|
130
|
+
"GET",
|
|
131
|
+
`${this._path}/${responseId}`,
|
|
132
|
+
);
|
|
133
|
+
return Response.fromObject(data);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async deleteStoredCompletion(responseId) {
|
|
137
|
+
return this._transport.request("DELETE", `${this._path}/${responseId}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function text(value) {
|
|
2
|
+
return { type: "text", text: value };
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function image(url) {
|
|
6
|
+
return { type: "image", image: url };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function video(url) {
|
|
10
|
+
return { type: "video", video: url };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function file(value, name) {
|
|
14
|
+
const part = { type: "file", file: value };
|
|
15
|
+
if (name !== undefined) part.name = name;
|
|
16
|
+
return part;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function message(role, parts) {
|
|
20
|
+
if (parts.length === 1 && typeof parts[0] === "string") {
|
|
21
|
+
return { role, content: parts[0] };
|
|
22
|
+
}
|
|
23
|
+
const content = parts.map((p) => (typeof p === "string" ? text(p) : p));
|
|
24
|
+
return { role, content };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function system(...parts) {
|
|
28
|
+
return message("system", parts);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function user(...parts) {
|
|
32
|
+
return message("user", parts);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function assistant(...parts) {
|
|
36
|
+
return message("assistant", parts);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function toolResult(callId, output) {
|
|
40
|
+
return { type: "function_call_output", call_id: callId, output };
|
|
41
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export type Role = "system" | "user" | "assistant" | string;
|
|
2
|
+
|
|
3
|
+
export interface ContentPart {
|
|
4
|
+
type: string;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Message {
|
|
9
|
+
role: Role;
|
|
10
|
+
content: string | ContentPart[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function text(value: string): ContentPart;
|
|
14
|
+
export function image(url: string): ContentPart;
|
|
15
|
+
export function video(url: string): ContentPart;
|
|
16
|
+
export function file(value: string, name?: string): ContentPart;
|
|
17
|
+
export function system(...parts: Array<string | ContentPart>): Message;
|
|
18
|
+
export function user(...parts: Array<string | ContentPart>): Message;
|
|
19
|
+
export function assistant(...parts: Array<string | ContentPart>): Message;
|
|
20
|
+
export function toolResult(callId: string, output: string): {
|
|
21
|
+
type: "function_call_output";
|
|
22
|
+
call_id: string;
|
|
23
|
+
output: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class Usage {
|
|
27
|
+
inputTokens: number;
|
|
28
|
+
outputTokens: number;
|
|
29
|
+
totalTokens: number;
|
|
30
|
+
costInUsdTicks: number | null;
|
|
31
|
+
raw: Record<string, unknown>;
|
|
32
|
+
static fromObject(data: unknown): Usage;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class FunctionCall {
|
|
36
|
+
name: string;
|
|
37
|
+
arguments: string;
|
|
38
|
+
callId: string | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class Response {
|
|
42
|
+
id: string | null;
|
|
43
|
+
model: string | null;
|
|
44
|
+
content: string;
|
|
45
|
+
reasoningContent: string | null;
|
|
46
|
+
output: Array<Record<string, unknown>>;
|
|
47
|
+
functionCalls: FunctionCall[];
|
|
48
|
+
usage: Usage;
|
|
49
|
+
raw: Record<string, unknown>;
|
|
50
|
+
static fromObject(data: unknown): Response;
|
|
51
|
+
toString(): string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class ResponseChunk {
|
|
55
|
+
type: string | null;
|
|
56
|
+
content: string;
|
|
57
|
+
reasoningContent: string;
|
|
58
|
+
status: string | null;
|
|
59
|
+
usage: Usage | null;
|
|
60
|
+
functionCall: FunctionCall | null;
|
|
61
|
+
raw: Record<string, unknown>;
|
|
62
|
+
static fromEvent(event: unknown): ResponseChunk;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface CreateChatOptions {
|
|
66
|
+
model: string;
|
|
67
|
+
storeMessages?: boolean;
|
|
68
|
+
previousResponseId?: string | null;
|
|
69
|
+
useEncryptedContent?: boolean;
|
|
70
|
+
temperature?: number;
|
|
71
|
+
topP?: number;
|
|
72
|
+
maxOutputTokens?: number;
|
|
73
|
+
reasoningEffort?: "low" | "medium" | "high";
|
|
74
|
+
tools?: unknown[];
|
|
75
|
+
toolChoice?: unknown;
|
|
76
|
+
[extra: string]: unknown;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class Chat {
|
|
80
|
+
model: string;
|
|
81
|
+
storeMessages: boolean;
|
|
82
|
+
previousResponseId: string | null;
|
|
83
|
+
messages: Array<Message | Record<string, unknown>>;
|
|
84
|
+
append(message: Message | Response | Record<string, unknown>): this;
|
|
85
|
+
sample(): Promise<Response>;
|
|
86
|
+
stream(): { response: Response; stream: AsyncIterableIterator<ResponseChunk> };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class ChatClient {
|
|
90
|
+
create(options: CreateChatOptions): Chat;
|
|
91
|
+
getStoredCompletion(responseId: string): Promise<Response>;
|
|
92
|
+
deleteStoredCompletion(responseId: string): Promise<Record<string, unknown>>;
|
|
93
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { Chat, ChatClient } from "./chat.js";
|
|
2
|
+
export {
|
|
3
|
+
Response,
|
|
4
|
+
ResponseChunk,
|
|
5
|
+
Usage,
|
|
6
|
+
FunctionCall,
|
|
7
|
+
} from "./types.js";
|
|
8
|
+
export {
|
|
9
|
+
system,
|
|
10
|
+
user,
|
|
11
|
+
assistant,
|
|
12
|
+
text,
|
|
13
|
+
image,
|
|
14
|
+
video,
|
|
15
|
+
file,
|
|
16
|
+
toolResult,
|
|
17
|
+
} from "./content.js";
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export class Usage {
|
|
2
|
+
constructor(data = {}) {
|
|
3
|
+
this.inputTokens = Number(data.input_tokens || 0);
|
|
4
|
+
this.outputTokens = Number(data.output_tokens || 0);
|
|
5
|
+
this.totalTokens = Number(data.total_tokens || 0);
|
|
6
|
+
this.costInUsdTicks =
|
|
7
|
+
data.cost_in_usd_ticks !== undefined ? data.cost_in_usd_ticks : null;
|
|
8
|
+
this.raw = data || {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static fromObject(data) {
|
|
12
|
+
return new Usage(data || {});
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class FunctionCall {
|
|
17
|
+
constructor({ name = "", arguments: args = "", call_id = null } = {}) {
|
|
18
|
+
this.name = name;
|
|
19
|
+
this.arguments = args;
|
|
20
|
+
this.callId = call_id;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function extractOutputText(output) {
|
|
25
|
+
const parts = [];
|
|
26
|
+
for (const item of output) {
|
|
27
|
+
if (item.type === "message") {
|
|
28
|
+
for (const block of item.content || []) {
|
|
29
|
+
if (block.type === "output_text" && block.text) parts.push(block.text);
|
|
30
|
+
}
|
|
31
|
+
} else if (item.type === "output_text" && item.text) {
|
|
32
|
+
parts.push(item.text);
|
|
33
|
+
} else if (item.type === "text" && item.text) {
|
|
34
|
+
parts.push(item.text);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return parts.join("");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function extractReasoning(output) {
|
|
41
|
+
for (const item of output) {
|
|
42
|
+
if (item.type === "reasoning") {
|
|
43
|
+
if (Array.isArray(item.summary)) {
|
|
44
|
+
const joined = item.summary.map((s) => s.text || "").join("");
|
|
45
|
+
if (joined) return joined;
|
|
46
|
+
}
|
|
47
|
+
if (item.encrypted_content) return item.encrypted_content;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class Response {
|
|
54
|
+
constructor(data) {
|
|
55
|
+
const output = Array.isArray(data.output) ? data.output : [];
|
|
56
|
+
this.id = data.id ?? null;
|
|
57
|
+
this.model = data.model ?? null;
|
|
58
|
+
this.content = extractOutputText(output);
|
|
59
|
+
this.reasoningContent = extractReasoning(output);
|
|
60
|
+
this.output = output;
|
|
61
|
+
this.functionCalls = (data.function_calls || []).map(
|
|
62
|
+
(fc) => new FunctionCall(fc),
|
|
63
|
+
);
|
|
64
|
+
this.usage = Usage.fromObject(data.usage);
|
|
65
|
+
this.raw = data;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static fromObject(data) {
|
|
69
|
+
return new Response(data);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
toString() {
|
|
73
|
+
return this.content;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class ResponseChunk {
|
|
78
|
+
constructor(event) {
|
|
79
|
+
this.type = event.type ?? null;
|
|
80
|
+
this.content = "";
|
|
81
|
+
this.reasoningContent = "";
|
|
82
|
+
this.status = null;
|
|
83
|
+
this.usage = null;
|
|
84
|
+
this.functionCall = null;
|
|
85
|
+
this.raw = event;
|
|
86
|
+
|
|
87
|
+
if (event.usage && typeof event.usage === "object") {
|
|
88
|
+
this.usage = Usage.fromObject(event.usage);
|
|
89
|
+
}
|
|
90
|
+
if (event.type === "reasoning_delta") {
|
|
91
|
+
this.reasoningContent = event.content || "";
|
|
92
|
+
} else if (event.type === "function_call") {
|
|
93
|
+
this.functionCall = new FunctionCall({
|
|
94
|
+
name: event.name,
|
|
95
|
+
arguments: event.arguments,
|
|
96
|
+
});
|
|
97
|
+
} else if (
|
|
98
|
+
["tool_update", "search_status", "search_query", "search_started"].includes(
|
|
99
|
+
event.type,
|
|
100
|
+
)
|
|
101
|
+
) {
|
|
102
|
+
this.status = event.status ?? null;
|
|
103
|
+
}
|
|
104
|
+
if (Array.isArray(event.choices) && event.choices.length) {
|
|
105
|
+
const delta = event.choices[0].delta || {};
|
|
106
|
+
this.content = delta.content || "";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static fromEvent(event) {
|
|
111
|
+
return new ResponseChunk(event);
|
|
112
|
+
}
|
|
113
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_BASE_URL,
|
|
3
|
+
DEFAULT_MAX_RETRIES,
|
|
4
|
+
DEFAULT_TIMEOUT,
|
|
5
|
+
} from "./constants.js";
|
|
6
|
+
import { CaetherError } from "./errors.js";
|
|
7
|
+
import { Transport } from "./transport.js";
|
|
8
|
+
import { ChatClient } from "./chat/chat.js";
|
|
9
|
+
|
|
10
|
+
function resolveApiKey(apiKey) {
|
|
11
|
+
const key = apiKey || (typeof process !== "undefined" && process.env?.CAETHER_API_KEY);
|
|
12
|
+
if (!key) {
|
|
13
|
+
throw new CaetherError(
|
|
14
|
+
"No API key provided. Pass { apiKey } or set the CAETHER_API_KEY environment variable.",
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
return key;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class Client {
|
|
21
|
+
constructor({
|
|
22
|
+
apiKey,
|
|
23
|
+
baseUrl,
|
|
24
|
+
timeout = DEFAULT_TIMEOUT,
|
|
25
|
+
maxRetries = DEFAULT_MAX_RETRIES,
|
|
26
|
+
defaultHeaders,
|
|
27
|
+
fetch: fetchImpl,
|
|
28
|
+
} = {}) {
|
|
29
|
+
this.apiKey = resolveApiKey(apiKey);
|
|
30
|
+
this.baseUrl = (
|
|
31
|
+
baseUrl ||
|
|
32
|
+
(typeof process !== "undefined" && process.env?.CAETHER_BASE_URL) ||
|
|
33
|
+
DEFAULT_BASE_URL
|
|
34
|
+
).replace(/\/+$/, "");
|
|
35
|
+
|
|
36
|
+
this._transport = new Transport({
|
|
37
|
+
baseUrl: this.baseUrl,
|
|
38
|
+
apiKey: this.apiKey,
|
|
39
|
+
timeout,
|
|
40
|
+
maxRetries,
|
|
41
|
+
defaultHeaders,
|
|
42
|
+
fetch: fetchImpl,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
this.chat = new ChatClient(this._transport, { path: "/chat/completions" });
|
|
46
|
+
this.code = new ChatClient(this._transport, { path: "/code/stream" });
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const DEFAULT_API_HOST = "api.caether.ai";
|
|
2
|
+
export const DEFAULT_BASE_URL = `https://${DEFAULT_API_HOST}`;
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_TIMEOUT = 3600000;
|
|
5
|
+
export const DEFAULT_MAX_RETRIES = 2;
|
|
6
|
+
|
|
7
|
+
export const USER_AGENT = "caetherai-js";
|
|
8
|
+
export const SDK_VERSION = "0.1.0";
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export class CaetherError extends Error {
|
|
2
|
+
constructor(message, { statusCode = null, code = null, body = null } = {}) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "CaetherError";
|
|
5
|
+
this.statusCode = statusCode;
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.body = body;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class APIConnectionError extends CaetherError {
|
|
12
|
+
constructor(message, opts) {
|
|
13
|
+
super(message, opts);
|
|
14
|
+
this.name = "APIConnectionError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class APITimeoutError extends APIConnectionError {
|
|
19
|
+
constructor(message, opts) {
|
|
20
|
+
super(message, opts);
|
|
21
|
+
this.name = "APITimeoutError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class APIStatusError extends CaetherError {
|
|
26
|
+
constructor(message, opts) {
|
|
27
|
+
super(message, opts);
|
|
28
|
+
this.name = "APIStatusError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class AuthenticationError extends APIStatusError {
|
|
33
|
+
constructor(m, o) { super(m, o); this.name = "AuthenticationError"; }
|
|
34
|
+
}
|
|
35
|
+
export class PermissionDeniedError extends APIStatusError {
|
|
36
|
+
constructor(m, o) { super(m, o); this.name = "PermissionDeniedError"; }
|
|
37
|
+
}
|
|
38
|
+
export class NotFoundError extends APIStatusError {
|
|
39
|
+
constructor(m, o) { super(m, o); this.name = "NotFoundError"; }
|
|
40
|
+
}
|
|
41
|
+
export class RateLimitError extends APIStatusError {
|
|
42
|
+
constructor(m, o) { super(m, o); this.name = "RateLimitError"; }
|
|
43
|
+
}
|
|
44
|
+
export class BadRequestError extends APIStatusError {
|
|
45
|
+
constructor(m, o) { super(m, o); this.name = "BadRequestError"; }
|
|
46
|
+
}
|
|
47
|
+
export class UnprocessableEntityError extends APIStatusError {
|
|
48
|
+
constructor(m, o) { super(m, o); this.name = "UnprocessableEntityError"; }
|
|
49
|
+
}
|
|
50
|
+
export class InternalServerError extends APIStatusError {
|
|
51
|
+
constructor(m, o) { super(m, o); this.name = "InternalServerError"; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const STATUS_TO_ERROR = {
|
|
55
|
+
400: BadRequestError,
|
|
56
|
+
401: AuthenticationError,
|
|
57
|
+
403: PermissionDeniedError,
|
|
58
|
+
404: NotFoundError,
|
|
59
|
+
422: UnprocessableEntityError,
|
|
60
|
+
429: RateLimitError,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export function errorFromResponse(statusCode, body) {
|
|
64
|
+
let message = `HTTP ${statusCode}`;
|
|
65
|
+
let code = null;
|
|
66
|
+
if (body && typeof body === "object") {
|
|
67
|
+
const err = body.error;
|
|
68
|
+
if (err && typeof err === "object") {
|
|
69
|
+
message = err.message || message;
|
|
70
|
+
code = err.code || null;
|
|
71
|
+
} else if (typeof err === "string") {
|
|
72
|
+
message = err;
|
|
73
|
+
} else if (typeof body.message === "string") {
|
|
74
|
+
message = body.message;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
let Cls;
|
|
78
|
+
if (STATUS_TO_ERROR[statusCode]) Cls = STATUS_TO_ERROR[statusCode];
|
|
79
|
+
else if (statusCode >= 500) Cls = InternalServerError;
|
|
80
|
+
else Cls = APIStatusError;
|
|
81
|
+
return new Cls(message, { statusCode, code, body });
|
|
82
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ChatClient } from "./chat/index.js";
|
|
2
|
+
|
|
3
|
+
export * as chat from "./chat/index.js";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_BASE_URL: string;
|
|
6
|
+
export const SDK_VERSION: string;
|
|
7
|
+
|
|
8
|
+
export interface ClientOptions {
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
timeout?: number;
|
|
12
|
+
maxRetries?: number;
|
|
13
|
+
defaultHeaders?: Record<string, string>;
|
|
14
|
+
fetch?: typeof fetch;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class Client {
|
|
18
|
+
apiKey: string;
|
|
19
|
+
baseUrl: string;
|
|
20
|
+
chat: ChatClient;
|
|
21
|
+
code: ChatClient;
|
|
22
|
+
constructor(options?: ClientOptions);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CaetherErrorOptions {
|
|
26
|
+
statusCode?: number | null;
|
|
27
|
+
code?: string | null;
|
|
28
|
+
body?: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class CaetherError extends Error {
|
|
32
|
+
statusCode: number | null;
|
|
33
|
+
code: string | null;
|
|
34
|
+
body: unknown;
|
|
35
|
+
constructor(message: string, options?: CaetherErrorOptions);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class APIConnectionError extends CaetherError {}
|
|
39
|
+
export class APITimeoutError extends APIConnectionError {}
|
|
40
|
+
export class APIStatusError extends CaetherError {}
|
|
41
|
+
export class AuthenticationError extends APIStatusError {}
|
|
42
|
+
export class PermissionDeniedError extends APIStatusError {}
|
|
43
|
+
export class NotFoundError extends APIStatusError {}
|
|
44
|
+
export class RateLimitError extends APIStatusError {}
|
|
45
|
+
export class BadRequestError extends APIStatusError {}
|
|
46
|
+
export class UnprocessableEntityError extends APIStatusError {}
|
|
47
|
+
export class InternalServerError extends APIStatusError {}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { Client } from "./client.js";
|
|
2
|
+
export { DEFAULT_BASE_URL, SDK_VERSION } from "./constants.js";
|
|
3
|
+
export {
|
|
4
|
+
CaetherError,
|
|
5
|
+
APIConnectionError,
|
|
6
|
+
APITimeoutError,
|
|
7
|
+
APIStatusError,
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
PermissionDeniedError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
RateLimitError,
|
|
12
|
+
BadRequestError,
|
|
13
|
+
UnprocessableEntityError,
|
|
14
|
+
InternalServerError,
|
|
15
|
+
} from "./errors.js";
|
|
16
|
+
|
|
17
|
+
export * as chat from "./chat/index.js";
|
package/src/transport.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_MAX_RETRIES,
|
|
3
|
+
DEFAULT_TIMEOUT,
|
|
4
|
+
SDK_VERSION,
|
|
5
|
+
USER_AGENT,
|
|
6
|
+
} from "./constants.js";
|
|
7
|
+
import {
|
|
8
|
+
APIConnectionError,
|
|
9
|
+
APITimeoutError,
|
|
10
|
+
errorFromResponse,
|
|
11
|
+
} from "./errors.js";
|
|
12
|
+
|
|
13
|
+
const RETRY_STATUS = new Set([408, 409, 429, 500, 502, 503, 504]);
|
|
14
|
+
|
|
15
|
+
function buildHeaders(apiKey, extra) {
|
|
16
|
+
const headers = {
|
|
17
|
+
Authorization: `Bearer ${apiKey}`,
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
Accept: "application/json",
|
|
20
|
+
"User-Agent": `${USER_AGENT}/${SDK_VERSION}`,
|
|
21
|
+
};
|
|
22
|
+
if (extra) {
|
|
23
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
24
|
+
if (v !== undefined && v !== null) headers[k] = v;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return headers;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function retryDelay(attempt) {
|
|
31
|
+
return Math.min(500 * 2 ** attempt, 8000);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
35
|
+
|
|
36
|
+
async function parseBody(response) {
|
|
37
|
+
const ct = response.headers.get("content-type") || "";
|
|
38
|
+
if (ct.includes("application/json")) {
|
|
39
|
+
try {
|
|
40
|
+
return await response.json();
|
|
41
|
+
} catch {
|
|
42
|
+
return await response.text();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return await response.text();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class Transport {
|
|
49
|
+
constructor({
|
|
50
|
+
baseUrl,
|
|
51
|
+
apiKey,
|
|
52
|
+
timeout = DEFAULT_TIMEOUT,
|
|
53
|
+
maxRetries = DEFAULT_MAX_RETRIES,
|
|
54
|
+
defaultHeaders = {},
|
|
55
|
+
fetch: fetchImpl,
|
|
56
|
+
}) {
|
|
57
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
58
|
+
this.apiKey = apiKey;
|
|
59
|
+
this.timeout = timeout;
|
|
60
|
+
this.maxRetries = maxRetries;
|
|
61
|
+
this.defaultHeaders = defaultHeaders || {};
|
|
62
|
+
this.fetch = fetchImpl || globalThis.fetch;
|
|
63
|
+
if (!this.fetch) {
|
|
64
|
+
throw new APIConnectionError(
|
|
65
|
+
"No fetch implementation available. Use Node 18+ or pass a fetch option.",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
url(path) {
|
|
71
|
+
return `${this.baseUrl}/${path.replace(/^\/+/, "")}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async request(method, path, { body, params, headers, accept } = {}) {
|
|
75
|
+
const merged = buildHeaders(this.apiKey, {
|
|
76
|
+
...this.defaultHeaders,
|
|
77
|
+
...(headers || {}),
|
|
78
|
+
});
|
|
79
|
+
if (accept) merged.Accept = accept;
|
|
80
|
+
|
|
81
|
+
let target = this.url(path);
|
|
82
|
+
if (params) {
|
|
83
|
+
const qs = new URLSearchParams(
|
|
84
|
+
Object.entries(params).filter(([, v]) => v !== undefined && v !== null),
|
|
85
|
+
).toString();
|
|
86
|
+
if (qs) target += `?${qs}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let lastErr = null;
|
|
90
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
93
|
+
let response;
|
|
94
|
+
try {
|
|
95
|
+
response = await this.fetch(target, {
|
|
96
|
+
method,
|
|
97
|
+
headers: merged,
|
|
98
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
99
|
+
signal: controller.signal,
|
|
100
|
+
});
|
|
101
|
+
} catch (e) {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
if (e.name === "AbortError") {
|
|
104
|
+
lastErr = new APITimeoutError("Request timed out");
|
|
105
|
+
} else {
|
|
106
|
+
lastErr = new APIConnectionError(`Connection error: ${e.message}`);
|
|
107
|
+
}
|
|
108
|
+
if (attempt < this.maxRetries) {
|
|
109
|
+
await sleep(retryDelay(attempt));
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
throw lastErr;
|
|
113
|
+
}
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
|
|
116
|
+
if (RETRY_STATUS.has(response.status) && attempt < this.maxRetries) {
|
|
117
|
+
await sleep(retryDelay(attempt));
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (response.status >= 400) {
|
|
121
|
+
throw errorFromResponse(response.status, await parseBody(response));
|
|
122
|
+
}
|
|
123
|
+
return parseBody(response);
|
|
124
|
+
}
|
|
125
|
+
throw lastErr || new APIConnectionError("Request failed");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async *stream(method, path, { body, headers } = {}) {
|
|
129
|
+
const merged = buildHeaders(this.apiKey, {
|
|
130
|
+
...this.defaultHeaders,
|
|
131
|
+
...(headers || {}),
|
|
132
|
+
});
|
|
133
|
+
merged.Accept = "text/event-stream";
|
|
134
|
+
|
|
135
|
+
const response = await this.fetch(this.url(path), {
|
|
136
|
+
method,
|
|
137
|
+
headers: merged,
|
|
138
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
139
|
+
});
|
|
140
|
+
if (response.status >= 400) {
|
|
141
|
+
throw errorFromResponse(response.status, await parseBody(response));
|
|
142
|
+
}
|
|
143
|
+
if (!response.body) return;
|
|
144
|
+
|
|
145
|
+
const decoder = new TextDecoder();
|
|
146
|
+
let buffer = "";
|
|
147
|
+
const reader = response.body.getReader();
|
|
148
|
+
while (true) {
|
|
149
|
+
const { done, value } = await reader.read();
|
|
150
|
+
if (done) break;
|
|
151
|
+
buffer += decoder.decode(value, { stream: true });
|
|
152
|
+
const lines = buffer.split("\n");
|
|
153
|
+
buffer = lines.pop();
|
|
154
|
+
for (const raw of lines) {
|
|
155
|
+
const line = raw.trim();
|
|
156
|
+
if (!line || !line.startsWith("data:")) continue;
|
|
157
|
+
const data = line.slice(5).trim();
|
|
158
|
+
if (data === "[DONE]") return;
|
|
159
|
+
try {
|
|
160
|
+
yield JSON.parse(data);
|
|
161
|
+
} catch {
|
|
162
|
+
/* skip malformed */
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|