@qualve/ai 0.0.1 → 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.
@@ -1,29 +0,0 @@
1
- {
2
- "name": "@qualve/openai",
3
- "version": "0.0.1",
4
- "description": "OpenAI provider for Qualve LLM tasks.",
5
- "type": "module",
6
- "engines": {
7
- "node": ">=23"
8
- },
9
- "main": "src/index.js",
10
- "exports": {
11
- ".": "./src/index.js"
12
- },
13
- "repository": {
14
- "type": "git",
15
- "url": "https://github.com/qualve/ai.git",
16
- "directory": "providers/openai"
17
- },
18
- "contributors": [
19
- "Dmitry Sharabin",
20
- "Lea Verou"
21
- ],
22
- "peerDependencies": {
23
- "qualve": "*"
24
- },
25
- "dependencies": {
26
- "@qualve/llm": "*",
27
- "openai": "^6.31.0"
28
- }
29
- }
@@ -1,182 +0,0 @@
1
- import OpenAIClient from "openai";
2
- import { LLMTask } from "@qualve/llm";
3
-
4
- export default class OpenAI extends LLMTask {
5
- static models = ["gpt-5.4", "gpt-5-mini", "gpt-5-nano"];
6
- static id = "openai";
7
- static name = "OpenAI";
8
- static capabilities = {
9
- outputSchema: true,
10
- thinkingLevel: true,
11
- };
12
-
13
- client = new OpenAIClient({
14
- apiKey: process.env.OPENAI_API_KEY,
15
- timeout: 30 * 60_000, // 30 minutes — LLM tasks with thinking can be very slow
16
- });
17
-
18
- async uploadFile (filepath, { mimeType, contents }) {
19
- let { name } = this.getFileInfo(filepath);
20
- return this.client.files.create({
21
- file: new File([contents], name, { type: mimeType }),
22
- purpose: "user_data",
23
- });
24
- }
25
-
26
- async listFiles () {
27
- const meta = [];
28
- const list = await this.client.files.list();
29
-
30
- for await (const file of list) {
31
- meta.push(file);
32
- }
33
-
34
- return meta;
35
- }
36
-
37
- async getFile (filepath) {
38
- let { name } = this.getFileInfo(filepath);
39
- const list = await this.listFiles();
40
- return list.find(file => file.filename === name);
41
- }
42
-
43
- async deleteFile (filepath) {
44
- const file = await this.getFile(filepath);
45
- if (!file) {
46
- return null;
47
- }
48
- await this.client.files.delete(file.id);
49
- }
50
-
51
- async createStream () {
52
- let { system, prompt, output, input = [] } = this;
53
- let responseSchema = output?.schema;
54
- let hasRootObject = output?.schemaType === "object";
55
-
56
- if (responseSchema) {
57
- // All object properties in the response schema must be required.
58
- // See https://developers.openai.com/api/docs/guides/structured-outputs#all-fields-must-be-required
59
- let obj = hasRootObject ? responseSchema.schema : responseSchema.schema.items;
60
- let properties = Object.keys(obj.properties);
61
- let notRequired = properties.filter(key => !obj.required.includes(key));
62
- if (notRequired.length) {
63
- for (let key of notRequired) {
64
- // Emulate an optional parameter by using a union type with null
65
- obj.properties[key].type = [obj.properties[key].type, "null"];
66
- }
67
- obj.required = properties;
68
- }
69
-
70
- // OpenAI requires a name for the response schema and strict mode
71
- responseSchema = { strict: true, name: "response", ...responseSchema };
72
-
73
- if (!hasRootObject) {
74
- // OpenAI only supports objects at the top level of output schemas.
75
- // See https://platform.openai.com/docs/guides/structured-outputs#root-objects-must-not-be-anyof-and-must-be-an-object
76
- responseSchema.schema = {
77
- type: "object",
78
- properties: {
79
- data: responseSchema.schema,
80
- },
81
- required: ["data"],
82
- additionalProperties: false,
83
- };
84
- }
85
- }
86
-
87
- const stream = this.client.responses.stream({
88
- model: this.model,
89
- background: true, // try to avoid hitting a client-side socket timeout after ~601s (10 minutes)
90
- store: true,
91
- reasoning: {
92
- effort: this.thinking ?? "medium",
93
- },
94
- input: [
95
- ...system.map(s => ({ type: "message", role: "system", content: s })),
96
- {
97
- type: "message",
98
- role: "user",
99
- content: [
100
- ...prompt.map(t => ({ type: "input_text", text: t })),
101
-
102
- // Include uploaded files as direct input_file blocks,
103
- // giving the model complete access to file contents (unlike file_search which returns chunks)
104
- ...input.flatMap(f => [
105
- { type: "input_text", text: this.inputFile(f) },
106
- { type: "input_file", file_id: f.remoteFile.id },
107
- ]),
108
- ],
109
- },
110
- ],
111
- text: {
112
- verbosity: "low",
113
- format: responseSchema,
114
- },
115
- });
116
-
117
- let incompleteReason;
118
- return {
119
- stream,
120
- transformChunk: chunk =>
121
- chunk.type === "response.output_text.delta" ? chunk.delta : "",
122
- transformResult: result => (hasRootObject || !responseSchema ? result : result.data),
123
- onChunk: chunk => {
124
- if (chunk.type === "response.incomplete") {
125
- incompleteReason = chunk.response?.incomplete_details?.reason ?? "unknown";
126
- }
127
- },
128
- onFinish: () => {
129
- if (!incompleteReason) {
130
- return {
131
- complete: true,
132
- reason: LLMTask.stopReasons.COMPLETE,
133
- reasonRaw: null,
134
- };
135
- }
136
-
137
- let reasons = {
138
- run_length: LLMTask.stopReasons.MAX_TOKENS,
139
- max_output_tokens: LLMTask.stopReasons.MAX_TOKENS,
140
- };
141
-
142
- return {
143
- complete: false,
144
- reason: reasons[incompleteReason] ?? LLMTask.stopReasons.UNKNOWN,
145
- reasonRaw: incompleteReason,
146
- };
147
- },
148
- };
149
- }
150
-
151
- getStatus (chunk) {
152
- // All supported events: https://platform.openai.com/docs/api-reference/responses-streaming
153
- let { type, item } = chunk;
154
- type = type.replace("response.", "");
155
- let message;
156
- if (type === "created") {
157
- message = "Processing the input...";
158
- }
159
- else if (type === "in_progress") {
160
- message = "Working on the response...";
161
- }
162
- else if (type.startsWith("output_item")) {
163
- if (item.type === "reasoning") {
164
- // If we don't do the model more "talkative" during reasoning, we won't get any additional info to display
165
- message = "Thinking...";
166
- }
167
- }
168
- else if (type.startsWith("web_search_call")) {
169
- message = "Searching the web...";
170
- }
171
- else if (type === "output_text.delta") {
172
- message = "Streaming the response...";
173
- }
174
- else if (type === "error") {
175
- message = `An error occurred: ${chunk.message}`;
176
- }
177
-
178
- return message;
179
- }
180
- }
181
-
182
- LLMTask.register(OpenAI);