@ci-kit/ai 0.0.0-stage → 0.0.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/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +87 -0
- package/dist/index.d.mts +126 -0
- package/dist/index.d.ts +126 -0
- package/dist/index.mjs +360 -0
- package/package.json +40 -3
- package/README.md +0 -3
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { writeFileSync, readFileSync } from 'node:fs';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { cac } from 'cac';
|
|
5
|
+
import { text, image, AiError } from './index.mjs';
|
|
6
|
+
import 'node:buffer';
|
|
7
|
+
import '@ci-kit/http';
|
|
8
|
+
|
|
9
|
+
const version = "0.0.1";
|
|
10
|
+
const pkg = {
|
|
11
|
+
version: version};
|
|
12
|
+
|
|
13
|
+
const cli = cac("ci-kit-ai");
|
|
14
|
+
function readPrompt(prompt, promptFile, vars = {}) {
|
|
15
|
+
let content = prompt ?? "";
|
|
16
|
+
if (promptFile) {
|
|
17
|
+
content = readFileSync(promptFile, "utf8");
|
|
18
|
+
}
|
|
19
|
+
if (!content) {
|
|
20
|
+
throw new Error("Provide --prompt or --prompt-file");
|
|
21
|
+
}
|
|
22
|
+
return content.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, name) => vars[name] ?? "");
|
|
23
|
+
}
|
|
24
|
+
function optionalNumber(value) {
|
|
25
|
+
return value === void 0 ? void 0 : Number(value);
|
|
26
|
+
}
|
|
27
|
+
function reportError(error) {
|
|
28
|
+
if (error instanceof AiError) {
|
|
29
|
+
process.stderr.write(`ci-kit-ai: ${error.describe()}
|
|
30
|
+
`);
|
|
31
|
+
} else {
|
|
32
|
+
process.stderr.write(`ci-kit-ai: ${error instanceof Error ? error.message : String(error)}
|
|
33
|
+
`);
|
|
34
|
+
}
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
cli.command("text", "Generate text with the configured provider").option("--prompt <text>", "Inline prompt").option("--prompt-file <path>", "Read prompt from file (supports {{var}})").option("--system <text>", "System message").option("--var <k=v>", "Template variable, repeatable").option("--max-tokens <n>", "Upper bound for generated tokens").option("--json", "Ask for a JSON object response").option("--temperature <n>", "Sampling temperature").option("--retries <n>", "Transport retries; use 0 for paid calls").option("--out <path>", "Write result to file").option("--timeout <ms>", "Request timeout in milliseconds").action(async (options) => {
|
|
38
|
+
const vars = {};
|
|
39
|
+
for (const [key, value] of Object.entries(options)) {
|
|
40
|
+
if (key.startsWith("var") && typeof value === "string") {
|
|
41
|
+
const [k, v = ""] = value.split("=");
|
|
42
|
+
vars[k] = v;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const result = await text({
|
|
47
|
+
prompt: readPrompt(options.prompt, options.promptFile, vars),
|
|
48
|
+
system: options.system,
|
|
49
|
+
maxTokens: optionalNumber(options.maxTokens),
|
|
50
|
+
json: options.json === true,
|
|
51
|
+
temperature: optionalNumber(options.temperature),
|
|
52
|
+
retries: optionalNumber(options.retries),
|
|
53
|
+
timeoutMs: optionalNumber(options.timeout)
|
|
54
|
+
});
|
|
55
|
+
const usage = result.usage ? ` tokens ${result.usage.input}/${result.usage.output}` : "";
|
|
56
|
+
if (options.out) {
|
|
57
|
+
writeFileSync(options.out, `${result.value}
|
|
58
|
+
`, "utf8");
|
|
59
|
+
process.stdout.write(`Wrote ${options.out} via ${result.provider} (${result.model})${usage}
|
|
60
|
+
`);
|
|
61
|
+
} else {
|
|
62
|
+
process.stdout.write(`${result.value}
|
|
63
|
+
`);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
reportError(error);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
cli.command("image", "Generate an image with the configured provider").option("--prompt <text>", "Image prompt").option("--prompt-file <path>", "Read prompt from file").option("--size <wxh>", "Image size; defaults to AI_IMAGE_SIZE or the provider default").option("--retries <n>", "Transport retries; use 0 for paid calls").option("--out <path>", "Output image path").option("--timeout <ms>", "Request timeout in milliseconds").action(async (options) => {
|
|
70
|
+
try {
|
|
71
|
+
const result = await image({
|
|
72
|
+
prompt: readPrompt(options.prompt, options.promptFile),
|
|
73
|
+
size: options.size,
|
|
74
|
+
retries: optionalNumber(options.retries),
|
|
75
|
+
timeoutMs: optionalNumber(options.timeout)
|
|
76
|
+
});
|
|
77
|
+
const out = options.out ?? "impression.png";
|
|
78
|
+
writeFileSync(out, result.value);
|
|
79
|
+
process.stdout.write(`Wrote ${out} (${result.value.length} bytes) via ${result.provider} (${result.model})
|
|
80
|
+
`);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
reportError(error);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
cli.help();
|
|
86
|
+
cli.version(pkg.version);
|
|
87
|
+
cli.parse();
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured AI failures.
|
|
3
|
+
*
|
|
4
|
+
* Callers branch on `kind`, never on message text. The message stays short and
|
|
5
|
+
* never carries credentials, request bodies or provider payloads, because CI
|
|
6
|
+
* logs are usually readable by anyone with repository access.
|
|
7
|
+
*/
|
|
8
|
+
type AiErrorKind = 'auth'
|
|
9
|
+
/** Rate limited or out of quota. */
|
|
10
|
+
| 'quota'
|
|
11
|
+
/** Bad request: unknown model, unsupported parameter, incompatible payload. */
|
|
12
|
+
| 'request'
|
|
13
|
+
/** Unreachable host, DNS failure, timeout. */
|
|
14
|
+
| 'network'
|
|
15
|
+
/** Provider returned 5xx. */
|
|
16
|
+
| 'server'
|
|
17
|
+
/** Request succeeded but the payload could not be used. */
|
|
18
|
+
| 'output'
|
|
19
|
+
/** Local configuration problem, before any network call. */
|
|
20
|
+
| 'config';
|
|
21
|
+
interface AiErrorInit {
|
|
22
|
+
kind: AiErrorKind;
|
|
23
|
+
message: string;
|
|
24
|
+
provider?: string;
|
|
25
|
+
model?: string;
|
|
26
|
+
status?: number;
|
|
27
|
+
/** Whether the same request may succeed later without changing anything. */
|
|
28
|
+
retryable?: boolean;
|
|
29
|
+
cause?: unknown;
|
|
30
|
+
}
|
|
31
|
+
declare class AiError extends Error {
|
|
32
|
+
readonly kind: AiErrorKind;
|
|
33
|
+
readonly provider?: string;
|
|
34
|
+
readonly model?: string;
|
|
35
|
+
readonly status?: number;
|
|
36
|
+
readonly retryable: boolean;
|
|
37
|
+
constructor(init: AiErrorInit);
|
|
38
|
+
/** One line for logs. Never includes the request body or the key. */
|
|
39
|
+
describe(): string;
|
|
40
|
+
}
|
|
41
|
+
declare function isAiError(error: unknown): error is AiError;
|
|
42
|
+
/** Map an HTTP status to a kind and whether a plain retry can help. */
|
|
43
|
+
declare function classifyStatus(status: number): {
|
|
44
|
+
kind: AiErrorKind;
|
|
45
|
+
retryable: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type TextProtocol = 'openai' | 'dashscope';
|
|
49
|
+
type ImageProtocol = 'seedream' | 'dashscope' | 'openai';
|
|
50
|
+
interface TextProvider {
|
|
51
|
+
/** Registry key, used by AI_TEXT_PROVIDER */
|
|
52
|
+
id: string;
|
|
53
|
+
/** Human readable vendor name */
|
|
54
|
+
label: string;
|
|
55
|
+
protocol: TextProtocol;
|
|
56
|
+
baseUrl: string;
|
|
57
|
+
model: string;
|
|
58
|
+
/** Environment variable holding the key; keys are never passed as flags */
|
|
59
|
+
apiKeyEnv: string;
|
|
60
|
+
}
|
|
61
|
+
interface ImageProvider {
|
|
62
|
+
id: string;
|
|
63
|
+
label: string;
|
|
64
|
+
protocol: ImageProtocol;
|
|
65
|
+
baseUrl: string;
|
|
66
|
+
model: string;
|
|
67
|
+
apiKeyEnv: string;
|
|
68
|
+
/** Provider specific default size, for example 1280x720 */
|
|
69
|
+
defaultSize?: string;
|
|
70
|
+
/** Size separator used by the API, defaults to x */
|
|
71
|
+
sizeSeparator?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
declare const imageProviders: Record<string, ImageProvider>;
|
|
75
|
+
declare const defaultImageProvider: string;
|
|
76
|
+
|
|
77
|
+
declare const textProviders: Record<string, TextProvider>;
|
|
78
|
+
declare const defaultTextProvider: string;
|
|
79
|
+
|
|
80
|
+
declare function resolveTextProvider(env?: NodeJS.ProcessEnv): TextProvider;
|
|
81
|
+
declare function resolveImageProvider(env?: NodeJS.ProcessEnv): ImageProvider;
|
|
82
|
+
|
|
83
|
+
interface TextOptions {
|
|
84
|
+
prompt: string;
|
|
85
|
+
system?: string;
|
|
86
|
+
/** Upper bound for the generated text. Providers that support it pass it through. */
|
|
87
|
+
maxTokens?: number;
|
|
88
|
+
/** Ask the provider for a JSON object. Unsupported providers throw instead of ignoring it. */
|
|
89
|
+
json?: boolean;
|
|
90
|
+
temperature?: number;
|
|
91
|
+
/**
|
|
92
|
+
* Transport retries. Defaults to the http package value.
|
|
93
|
+
* Pass 0 for paid or non-idempotent calls: the provider may already have
|
|
94
|
+
* completed the request, and retrying charges again.
|
|
95
|
+
*/
|
|
96
|
+
retries?: number;
|
|
97
|
+
timeoutMs?: number;
|
|
98
|
+
env?: NodeJS.ProcessEnv;
|
|
99
|
+
}
|
|
100
|
+
interface ImageOptions {
|
|
101
|
+
prompt: string;
|
|
102
|
+
size?: string;
|
|
103
|
+
retries?: number;
|
|
104
|
+
timeoutMs?: number;
|
|
105
|
+
env?: NodeJS.ProcessEnv;
|
|
106
|
+
}
|
|
107
|
+
interface TokenUsage {
|
|
108
|
+
input: number;
|
|
109
|
+
output: number;
|
|
110
|
+
}
|
|
111
|
+
interface AiResult<T> {
|
|
112
|
+
value: T;
|
|
113
|
+
provider: string;
|
|
114
|
+
model: string;
|
|
115
|
+
/** Present when the provider reports token usage. */
|
|
116
|
+
usage?: TokenUsage;
|
|
117
|
+
/** Raw provider finish reason, for example `stop` or `length`. */
|
|
118
|
+
finishReason?: string;
|
|
119
|
+
}
|
|
120
|
+
declare function text(options: TextOptions): Promise<AiResult<string>>;
|
|
121
|
+
declare function image(options: ImageOptions): Promise<AiResult<Uint8Array>>;
|
|
122
|
+
type ImageFormat = 'png' | 'jpeg';
|
|
123
|
+
declare function detectImageFormat(bytes: Uint8Array): ImageFormat | null;
|
|
124
|
+
|
|
125
|
+
export { AiError, classifyStatus, defaultImageProvider, defaultTextProvider, detectImageFormat, image, imageProviders, isAiError, resolveImageProvider, resolveTextProvider, text, textProviders };
|
|
126
|
+
export type { AiErrorInit, AiErrorKind, AiResult, ImageFormat, ImageOptions, ImageProtocol, ImageProvider, TextOptions, TextProtocol, TextProvider, TokenUsage };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured AI failures.
|
|
3
|
+
*
|
|
4
|
+
* Callers branch on `kind`, never on message text. The message stays short and
|
|
5
|
+
* never carries credentials, request bodies or provider payloads, because CI
|
|
6
|
+
* logs are usually readable by anyone with repository access.
|
|
7
|
+
*/
|
|
8
|
+
type AiErrorKind = 'auth'
|
|
9
|
+
/** Rate limited or out of quota. */
|
|
10
|
+
| 'quota'
|
|
11
|
+
/** Bad request: unknown model, unsupported parameter, incompatible payload. */
|
|
12
|
+
| 'request'
|
|
13
|
+
/** Unreachable host, DNS failure, timeout. */
|
|
14
|
+
| 'network'
|
|
15
|
+
/** Provider returned 5xx. */
|
|
16
|
+
| 'server'
|
|
17
|
+
/** Request succeeded but the payload could not be used. */
|
|
18
|
+
| 'output'
|
|
19
|
+
/** Local configuration problem, before any network call. */
|
|
20
|
+
| 'config';
|
|
21
|
+
interface AiErrorInit {
|
|
22
|
+
kind: AiErrorKind;
|
|
23
|
+
message: string;
|
|
24
|
+
provider?: string;
|
|
25
|
+
model?: string;
|
|
26
|
+
status?: number;
|
|
27
|
+
/** Whether the same request may succeed later without changing anything. */
|
|
28
|
+
retryable?: boolean;
|
|
29
|
+
cause?: unknown;
|
|
30
|
+
}
|
|
31
|
+
declare class AiError extends Error {
|
|
32
|
+
readonly kind: AiErrorKind;
|
|
33
|
+
readonly provider?: string;
|
|
34
|
+
readonly model?: string;
|
|
35
|
+
readonly status?: number;
|
|
36
|
+
readonly retryable: boolean;
|
|
37
|
+
constructor(init: AiErrorInit);
|
|
38
|
+
/** One line for logs. Never includes the request body or the key. */
|
|
39
|
+
describe(): string;
|
|
40
|
+
}
|
|
41
|
+
declare function isAiError(error: unknown): error is AiError;
|
|
42
|
+
/** Map an HTTP status to a kind and whether a plain retry can help. */
|
|
43
|
+
declare function classifyStatus(status: number): {
|
|
44
|
+
kind: AiErrorKind;
|
|
45
|
+
retryable: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type TextProtocol = 'openai' | 'dashscope';
|
|
49
|
+
type ImageProtocol = 'seedream' | 'dashscope' | 'openai';
|
|
50
|
+
interface TextProvider {
|
|
51
|
+
/** Registry key, used by AI_TEXT_PROVIDER */
|
|
52
|
+
id: string;
|
|
53
|
+
/** Human readable vendor name */
|
|
54
|
+
label: string;
|
|
55
|
+
protocol: TextProtocol;
|
|
56
|
+
baseUrl: string;
|
|
57
|
+
model: string;
|
|
58
|
+
/** Environment variable holding the key; keys are never passed as flags */
|
|
59
|
+
apiKeyEnv: string;
|
|
60
|
+
}
|
|
61
|
+
interface ImageProvider {
|
|
62
|
+
id: string;
|
|
63
|
+
label: string;
|
|
64
|
+
protocol: ImageProtocol;
|
|
65
|
+
baseUrl: string;
|
|
66
|
+
model: string;
|
|
67
|
+
apiKeyEnv: string;
|
|
68
|
+
/** Provider specific default size, for example 1280x720 */
|
|
69
|
+
defaultSize?: string;
|
|
70
|
+
/** Size separator used by the API, defaults to x */
|
|
71
|
+
sizeSeparator?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
declare const imageProviders: Record<string, ImageProvider>;
|
|
75
|
+
declare const defaultImageProvider: string;
|
|
76
|
+
|
|
77
|
+
declare const textProviders: Record<string, TextProvider>;
|
|
78
|
+
declare const defaultTextProvider: string;
|
|
79
|
+
|
|
80
|
+
declare function resolveTextProvider(env?: NodeJS.ProcessEnv): TextProvider;
|
|
81
|
+
declare function resolveImageProvider(env?: NodeJS.ProcessEnv): ImageProvider;
|
|
82
|
+
|
|
83
|
+
interface TextOptions {
|
|
84
|
+
prompt: string;
|
|
85
|
+
system?: string;
|
|
86
|
+
/** Upper bound for the generated text. Providers that support it pass it through. */
|
|
87
|
+
maxTokens?: number;
|
|
88
|
+
/** Ask the provider for a JSON object. Unsupported providers throw instead of ignoring it. */
|
|
89
|
+
json?: boolean;
|
|
90
|
+
temperature?: number;
|
|
91
|
+
/**
|
|
92
|
+
* Transport retries. Defaults to the http package value.
|
|
93
|
+
* Pass 0 for paid or non-idempotent calls: the provider may already have
|
|
94
|
+
* completed the request, and retrying charges again.
|
|
95
|
+
*/
|
|
96
|
+
retries?: number;
|
|
97
|
+
timeoutMs?: number;
|
|
98
|
+
env?: NodeJS.ProcessEnv;
|
|
99
|
+
}
|
|
100
|
+
interface ImageOptions {
|
|
101
|
+
prompt: string;
|
|
102
|
+
size?: string;
|
|
103
|
+
retries?: number;
|
|
104
|
+
timeoutMs?: number;
|
|
105
|
+
env?: NodeJS.ProcessEnv;
|
|
106
|
+
}
|
|
107
|
+
interface TokenUsage {
|
|
108
|
+
input: number;
|
|
109
|
+
output: number;
|
|
110
|
+
}
|
|
111
|
+
interface AiResult<T> {
|
|
112
|
+
value: T;
|
|
113
|
+
provider: string;
|
|
114
|
+
model: string;
|
|
115
|
+
/** Present when the provider reports token usage. */
|
|
116
|
+
usage?: TokenUsage;
|
|
117
|
+
/** Raw provider finish reason, for example `stop` or `length`. */
|
|
118
|
+
finishReason?: string;
|
|
119
|
+
}
|
|
120
|
+
declare function text(options: TextOptions): Promise<AiResult<string>>;
|
|
121
|
+
declare function image(options: ImageOptions): Promise<AiResult<Uint8Array>>;
|
|
122
|
+
type ImageFormat = 'png' | 'jpeg';
|
|
123
|
+
declare function detectImageFormat(bytes: Uint8Array): ImageFormat | null;
|
|
124
|
+
|
|
125
|
+
export { AiError, classifyStatus, defaultImageProvider, defaultTextProvider, detectImageFormat, image, imageProviders, isAiError, resolveImageProvider, resolveTextProvider, text, textProviders };
|
|
126
|
+
export type { AiErrorInit, AiErrorKind, AiResult, ImageFormat, ImageOptions, ImageProtocol, ImageProvider, TextOptions, TextProtocol, TextProvider, TokenUsage };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { requestJson, HttpError, request } from '@ci-kit/http';
|
|
4
|
+
|
|
5
|
+
class AiError extends Error {
|
|
6
|
+
kind;
|
|
7
|
+
provider;
|
|
8
|
+
model;
|
|
9
|
+
status;
|
|
10
|
+
retryable;
|
|
11
|
+
constructor(init) {
|
|
12
|
+
super(init.message, { cause: init.cause });
|
|
13
|
+
this.name = "AiError";
|
|
14
|
+
this.kind = init.kind;
|
|
15
|
+
this.provider = init.provider;
|
|
16
|
+
this.model = init.model;
|
|
17
|
+
this.status = init.status;
|
|
18
|
+
this.retryable = init.retryable ?? false;
|
|
19
|
+
}
|
|
20
|
+
/** One line for logs. Never includes the request body or the key. */
|
|
21
|
+
describe() {
|
|
22
|
+
const parts = [this.kind];
|
|
23
|
+
if (this.provider) {
|
|
24
|
+
parts.push(this.provider);
|
|
25
|
+
}
|
|
26
|
+
if (this.model) {
|
|
27
|
+
parts.push(this.model);
|
|
28
|
+
}
|
|
29
|
+
if (this.status !== void 0) {
|
|
30
|
+
parts.push(`HTTP ${this.status}`);
|
|
31
|
+
}
|
|
32
|
+
return `${parts.join(" / ")}: ${this.message}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function isAiError(error) {
|
|
36
|
+
return error instanceof AiError;
|
|
37
|
+
}
|
|
38
|
+
function classifyStatus(status) {
|
|
39
|
+
if (status === 401 || status === 403) {
|
|
40
|
+
return { kind: "auth", retryable: false };
|
|
41
|
+
}
|
|
42
|
+
if (status === 429) {
|
|
43
|
+
return { kind: "quota", retryable: true };
|
|
44
|
+
}
|
|
45
|
+
if (status === 408) {
|
|
46
|
+
return { kind: "network", retryable: true };
|
|
47
|
+
}
|
|
48
|
+
if (status >= 500) {
|
|
49
|
+
return { kind: "server", retryable: true };
|
|
50
|
+
}
|
|
51
|
+
return { kind: "request", retryable: false };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const dashscopeImage = {
|
|
55
|
+
id: "dashscope-image",
|
|
56
|
+
label: "Alibaba DashScope (image)",
|
|
57
|
+
protocol: "dashscope",
|
|
58
|
+
baseUrl: "https://dashscope.aliyuncs.com/api/v1",
|
|
59
|
+
model: "qwen-image-3.0",
|
|
60
|
+
apiKeyEnv: "AI_IMAGE_API_KEY",
|
|
61
|
+
defaultSize: "1280x720",
|
|
62
|
+
sizeSeparator: "*"
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const seedreamImage = {
|
|
66
|
+
id: "seedream",
|
|
67
|
+
label: "Volcengine Ark Seedream",
|
|
68
|
+
protocol: "seedream",
|
|
69
|
+
baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
|
|
70
|
+
model: "doubao-seedream-5-0-flash-260915",
|
|
71
|
+
apiKeyEnv: "AI_IMAGE_API_KEY",
|
|
72
|
+
defaultSize: "1280x720"
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const imageProviders = {
|
|
76
|
+
[seedreamImage.id]: seedreamImage,
|
|
77
|
+
[dashscopeImage.id]: dashscopeImage
|
|
78
|
+
};
|
|
79
|
+
const defaultImageProvider = seedreamImage.id;
|
|
80
|
+
|
|
81
|
+
const dashscopeText = {
|
|
82
|
+
id: "dashscope",
|
|
83
|
+
label: "Alibaba DashScope (compatible mode)",
|
|
84
|
+
protocol: "openai",
|
|
85
|
+
baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
86
|
+
model: "ZHIPU/GLM-5.3-Flash",
|
|
87
|
+
apiKeyEnv: "AI_API_KEY"
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const openaiText = {
|
|
91
|
+
id: "openai",
|
|
92
|
+
label: "OpenAI (chat completions)",
|
|
93
|
+
protocol: "openai",
|
|
94
|
+
baseUrl: "https://api.openai.com/v1",
|
|
95
|
+
model: "gpt-4.1-mini",
|
|
96
|
+
apiKeyEnv: "AI_API_KEY"
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const textProviders = {
|
|
100
|
+
[dashscopeText.id]: dashscopeText,
|
|
101
|
+
[openaiText.id]: openaiText
|
|
102
|
+
};
|
|
103
|
+
const defaultTextProvider = dashscopeText.id;
|
|
104
|
+
|
|
105
|
+
function resolveTextProvider(env = process.env) {
|
|
106
|
+
const id = env.AI_TEXT_PROVIDER || defaultTextProvider;
|
|
107
|
+
const provider = textProviders[id];
|
|
108
|
+
if (!provider) {
|
|
109
|
+
throw new Error(`Unknown AI_TEXT_PROVIDER: ${id}. Available: ${Object.keys(textProviders).join(", ")}`);
|
|
110
|
+
}
|
|
111
|
+
return provider;
|
|
112
|
+
}
|
|
113
|
+
function resolveImageProvider(env = process.env) {
|
|
114
|
+
const id = env.AI_IMAGE_PROVIDER || defaultImageProvider;
|
|
115
|
+
const provider = imageProviders[id];
|
|
116
|
+
if (!provider) {
|
|
117
|
+
throw new Error(`Unknown AI_IMAGE_PROVIDER: ${id}. Available: ${Object.keys(imageProviders).join(", ")}`);
|
|
118
|
+
}
|
|
119
|
+
return provider;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const PNG_MAGIC = [137, 80, 78, 71];
|
|
123
|
+
const JPEG_MAGIC = [255, 216, 255];
|
|
124
|
+
function apiKeyOf(env, keyEnv, provider) {
|
|
125
|
+
const key = env[keyEnv]?.trim();
|
|
126
|
+
if (!key) {
|
|
127
|
+
throw new AiError({
|
|
128
|
+
kind: "config",
|
|
129
|
+
provider,
|
|
130
|
+
message: `Missing ${keyEnv}; set it in the environment or .env store`
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return key;
|
|
134
|
+
}
|
|
135
|
+
function trimSlash(baseUrl) {
|
|
136
|
+
return baseUrl.replace(/\/+$/, "");
|
|
137
|
+
}
|
|
138
|
+
function wrapError(error, provider, context) {
|
|
139
|
+
if (error instanceof AiError) {
|
|
140
|
+
return error;
|
|
141
|
+
}
|
|
142
|
+
if (error instanceof HttpError) {
|
|
143
|
+
const { kind, retryable } = classifyStatus(error.status);
|
|
144
|
+
return new AiError({
|
|
145
|
+
kind,
|
|
146
|
+
provider: provider.id,
|
|
147
|
+
model: provider.model,
|
|
148
|
+
status: error.status,
|
|
149
|
+
retryable,
|
|
150
|
+
// The body is deliberately dropped: it can echo the prompt or request.
|
|
151
|
+
message: `${context} rejected by the provider`,
|
|
152
|
+
cause: error
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (error instanceof Error) {
|
|
156
|
+
const timedOut = error.name === "TimeoutError" || error.name === "AbortError";
|
|
157
|
+
return new AiError({
|
|
158
|
+
kind: "network",
|
|
159
|
+
provider: provider.id,
|
|
160
|
+
model: provider.model,
|
|
161
|
+
// A timeout after the provider accepted the request must not be retried
|
|
162
|
+
// blindly, so it is reported as non-retryable by default.
|
|
163
|
+
retryable: !timedOut,
|
|
164
|
+
message: timedOut ? `${context} timed out` : `${context} request failed: ${error.message}`,
|
|
165
|
+
cause: error
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return new AiError({
|
|
169
|
+
kind: "network",
|
|
170
|
+
provider: provider.id,
|
|
171
|
+
model: provider.model,
|
|
172
|
+
message: `${context} failed`,
|
|
173
|
+
cause: error
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function supportsJsonMode(provider) {
|
|
177
|
+
return provider.protocol === "openai" || provider.protocol === "dashscope";
|
|
178
|
+
}
|
|
179
|
+
async function text(options) {
|
|
180
|
+
const env = options.env ?? process.env;
|
|
181
|
+
const provider = resolveTextProvider(env);
|
|
182
|
+
if (options.json && !supportsJsonMode(provider)) {
|
|
183
|
+
throw new AiError({
|
|
184
|
+
kind: "config",
|
|
185
|
+
provider: provider.id,
|
|
186
|
+
model: provider.model,
|
|
187
|
+
message: `Provider ${provider.id} does not support JSON output; remove json: true or change provider`
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const key = apiKeyOf(env, provider.apiKeyEnv, provider.id);
|
|
191
|
+
const messages = [
|
|
192
|
+
...options.system ? [
|
|
193
|
+
{ role: "system", content: options.system }
|
|
194
|
+
] : [],
|
|
195
|
+
{ role: "user", content: options.prompt }
|
|
196
|
+
];
|
|
197
|
+
const body = {
|
|
198
|
+
model: env.AI_TEXT_MODEL || provider.model,
|
|
199
|
+
messages,
|
|
200
|
+
stream: false
|
|
201
|
+
};
|
|
202
|
+
if (options.maxTokens !== void 0) {
|
|
203
|
+
body.max_tokens = options.maxTokens;
|
|
204
|
+
}
|
|
205
|
+
if (options.temperature !== void 0) {
|
|
206
|
+
body.temperature = options.temperature;
|
|
207
|
+
}
|
|
208
|
+
if (options.json) {
|
|
209
|
+
body.response_format = { type: "json_object" };
|
|
210
|
+
}
|
|
211
|
+
let data;
|
|
212
|
+
try {
|
|
213
|
+
data = await requestJson(`${trimSlash(provider.baseUrl)}/chat/completions`, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: { "authorization": `Bearer ${key}`, "content-type": "application/json" },
|
|
216
|
+
body: JSON.stringify(body)
|
|
217
|
+
}, { timeoutMs: options.timeoutMs, retries: options.retries });
|
|
218
|
+
} catch (error) {
|
|
219
|
+
throw wrapError(error, provider, "text");
|
|
220
|
+
}
|
|
221
|
+
const choice = data.choices?.[0];
|
|
222
|
+
const value = choice?.message?.content?.trim();
|
|
223
|
+
if (!value) {
|
|
224
|
+
throw new AiError({
|
|
225
|
+
kind: "output",
|
|
226
|
+
provider: provider.id,
|
|
227
|
+
model: provider.model,
|
|
228
|
+
message: "Empty text response from provider"
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
const finishReason = choice?.finish_reason;
|
|
232
|
+
if (finishReason && finishReason !== "stop") {
|
|
233
|
+
throw new AiError({
|
|
234
|
+
kind: "output",
|
|
235
|
+
provider: provider.id,
|
|
236
|
+
model: provider.model,
|
|
237
|
+
retryable: finishReason === "length",
|
|
238
|
+
message: `Text generation ended with finish_reason=${finishReason}; the content is incomplete`
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
value,
|
|
243
|
+
provider: provider.id,
|
|
244
|
+
model: String(body.model),
|
|
245
|
+
usage: data.usage ? { input: Number(data.usage.prompt_tokens) || 0, output: Number(data.usage.completion_tokens) || 0 } : void 0,
|
|
246
|
+
finishReason
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
async function image(options) {
|
|
250
|
+
const env = options.env ?? process.env;
|
|
251
|
+
const provider = resolveImageProvider(env);
|
|
252
|
+
const key = apiKeyOf(env, provider.apiKeyEnv, provider.id);
|
|
253
|
+
const rawSize = options.size ?? env.AI_IMAGE_SIZE ?? provider.defaultSize ?? "1280x720";
|
|
254
|
+
const size = normalizeSize(rawSize, provider);
|
|
255
|
+
const body = provider.protocol === "seedream" ? {
|
|
256
|
+
model: env.AI_IMAGE_MODEL || provider.model,
|
|
257
|
+
prompt: options.prompt,
|
|
258
|
+
size,
|
|
259
|
+
response_format: "b64_json"
|
|
260
|
+
} : {
|
|
261
|
+
model: env.AI_IMAGE_MODEL || provider.model,
|
|
262
|
+
prompt: options.prompt,
|
|
263
|
+
size,
|
|
264
|
+
n: 1
|
|
265
|
+
};
|
|
266
|
+
let data;
|
|
267
|
+
try {
|
|
268
|
+
data = await requestJson(`${trimSlash(provider.baseUrl)}/images/generations`, {
|
|
269
|
+
method: "POST",
|
|
270
|
+
headers: { "authorization": `Bearer ${key}`, "content-type": "application/json" },
|
|
271
|
+
body: JSON.stringify(body)
|
|
272
|
+
}, { timeoutMs: options.timeoutMs, retries: options.retries });
|
|
273
|
+
} catch (error) {
|
|
274
|
+
throw wrapError(error, provider, "image");
|
|
275
|
+
}
|
|
276
|
+
const items = data.data;
|
|
277
|
+
if (!Array.isArray(items) || items.length !== 1) {
|
|
278
|
+
throw new AiError({
|
|
279
|
+
kind: "output",
|
|
280
|
+
provider: provider.id,
|
|
281
|
+
model: provider.model,
|
|
282
|
+
message: `Expected exactly 1 image, received ${Array.isArray(items) ? items.length : "none"}`
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
const item = items[0];
|
|
286
|
+
if (item.error) {
|
|
287
|
+
throw new AiError({
|
|
288
|
+
kind: "output",
|
|
289
|
+
provider: provider.id,
|
|
290
|
+
model: provider.model,
|
|
291
|
+
// Only the code is surfaced; provider messages may echo the prompt.
|
|
292
|
+
message: `Image item failed${item.error.code ? ` with code ${item.error.code}` : ""}`
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
const value = await readImagePayload(item, provider, options.timeoutMs, options.retries);
|
|
296
|
+
const format = detectImageFormat(value);
|
|
297
|
+
if (!format) {
|
|
298
|
+
throw new AiError({
|
|
299
|
+
kind: "output",
|
|
300
|
+
provider: provider.id,
|
|
301
|
+
model: provider.model,
|
|
302
|
+
message: "Image payload is neither PNG nor JPEG"
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
value,
|
|
307
|
+
provider: provider.id,
|
|
308
|
+
model: String(body.model)
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function normalizeSize(size, provider) {
|
|
312
|
+
const [width, height] = size.toLowerCase().split("x");
|
|
313
|
+
if (!width || !height || !Number.isFinite(Number(width)) || !Number.isFinite(Number(height))) {
|
|
314
|
+
throw new AiError({
|
|
315
|
+
kind: "config",
|
|
316
|
+
provider: provider.id,
|
|
317
|
+
model: provider.model,
|
|
318
|
+
message: `Invalid size: ${size}. Expected WxH, for example 1280x720`
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return `${width}${provider.sizeSeparator ?? "x"}${height}`;
|
|
322
|
+
}
|
|
323
|
+
async function readImagePayload(item, provider, timeoutMs, retries) {
|
|
324
|
+
if (item.b64_json) {
|
|
325
|
+
return new Uint8Array(Buffer.from(item.b64_json, "base64"));
|
|
326
|
+
}
|
|
327
|
+
if (item.url) {
|
|
328
|
+
if (!/^https:\/\//i.test(item.url)) {
|
|
329
|
+
throw new AiError({
|
|
330
|
+
kind: "output",
|
|
331
|
+
provider: provider.id,
|
|
332
|
+
model: provider.model,
|
|
333
|
+
message: "Image URL must be HTTPS"
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const response = await request(item.url, {}, { timeoutMs, retries });
|
|
338
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
339
|
+
} catch (error) {
|
|
340
|
+
throw wrapError(error, provider, "image download");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
throw new AiError({
|
|
344
|
+
kind: "output",
|
|
345
|
+
provider: provider.id,
|
|
346
|
+
model: provider.model,
|
|
347
|
+
message: "Empty image response from provider"
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
function detectImageFormat(bytes) {
|
|
351
|
+
if (bytes.length >= 4 && PNG_MAGIC.every((byte, index) => bytes[index] === byte)) {
|
|
352
|
+
return "png";
|
|
353
|
+
}
|
|
354
|
+
if (bytes.length >= 3 && JPEG_MAGIC.every((byte, index) => bytes[index] === byte)) {
|
|
355
|
+
return "jpeg";
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export { AiError, classifyStatus, defaultImageProvider, defaultTextProvider, detectImageFormat, image, imageProviders, isAiError, resolveImageProvider, resolveTextProvider, text, textProviders };
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ci-kit/ai",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "Multi-provider AI client for text and image generation",
|
|
6
|
+
"author": "imba97",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/cikit-js/ci-kit#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/cikit-js/ci-kit.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/cikit-js/ci-kit/issues"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.mts",
|
|
19
|
+
"import": "./dist/index.mjs"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"main": "./dist/index.mjs",
|
|
23
|
+
"module": "./dist/index.mjs",
|
|
24
|
+
"types": "./dist/index.d.mts",
|
|
25
|
+
"bin": {
|
|
26
|
+
"ci-kit-ai": "dist/cli.mjs"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@ci-kit/http": "0.0.1",
|
|
33
|
+
"cac": "^7.1.0"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "unbuild",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"stub": "unbuild --stub"
|
|
42
|
+
}
|
|
6
43
|
}
|
package/README.md
DELETED