opencode-cmd-provider 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/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -0
- package/dist/src/env.d.ts +7 -0
- package/dist/src/env.js +24 -0
- package/dist/src/plugin/auth-server.d.ts +30 -0
- package/dist/src/plugin/auth-server.js +158 -0
- package/dist/src/plugin/auth.d.ts +6 -0
- package/dist/src/plugin/auth.js +38 -0
- package/dist/src/plugin/index.d.ts +6 -0
- package/dist/src/plugin/index.js +39 -0
- package/dist/src/plugin/models.d.ts +7 -0
- package/dist/src/plugin/models.js +50 -0
- package/dist/src/provider/aisdk-types.d.ts +9 -0
- package/dist/src/provider/aisdk-types.js +1 -0
- package/dist/src/provider/auth-key.d.ts +7 -0
- package/dist/src/provider/auth-key.js +65 -0
- package/dist/src/provider/command-code-model.d.ts +39 -0
- package/dist/src/provider/command-code-model.js +425 -0
- package/dist/src/provider/converters.d.ts +33 -0
- package/dist/src/provider/converters.js +256 -0
- package/dist/src/provider/cost.d.ts +19 -0
- package/dist/src/provider/cost.js +19 -0
- package/dist/src/provider/index.d.ts +5 -0
- package/dist/src/provider/index.js +9 -0
- package/dist/src/provider/json-schema.d.ts +1 -0
- package/dist/src/provider/json-schema.js +374 -0
- package/dist/src/provider/modalities.d.ts +9 -0
- package/dist/src/provider/modalities.js +53 -0
- package/dist/src/provider/models.d.ts +29 -0
- package/dist/src/provider/models.js +229 -0
- package/dist/src/provider/pricing.d.ts +24 -0
- package/dist/src/provider/pricing.js +188 -0
- package/dist/src/provider/project-slug.d.ts +1 -0
- package/dist/src/provider/project-slug.js +10 -0
- package/dist/src/provider/reasoning.d.ts +29 -0
- package/dist/src/provider/reasoning.js +74 -0
- package/dist/src/provider/redact.d.ts +2 -0
- package/dist/src/provider/redact.js +59 -0
- package/dist/src/provider/retry.d.ts +8 -0
- package/dist/src/provider/retry.js +83 -0
- package/dist/src/provider/stream.d.ts +5 -0
- package/dist/src/provider/stream.js +105 -0
- package/package.json +58 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/provider/redact.ts — credential redaction (PLAN #3 Part B)
|
|
2
|
+
//
|
|
3
|
+
// Port of pi-commandcode-provider/src/overflow.ts, keeping only the
|
|
4
|
+
// redaction functions. Overflow-normalization functions and the
|
|
5
|
+
// CommandCodeMessageLike type are dropped: opencode has its own
|
|
6
|
+
// context-overflow handling; if a context-overflow error surfaces it
|
|
7
|
+
// arrives as a plain AI SDK error and opencode's own compaction handles it.
|
|
8
|
+
// Applied to every error surfaced to opencode — AI SDK errors must never
|
|
9
|
+
// leak credentials (DESIGN §6.6).
|
|
10
|
+
const BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi;
|
|
11
|
+
const CREDENTIAL_PATTERN = /\b(?:api[-_ ]?key|apikey|access[-_ ]?token|refresh[-_ ]?token|token|secret|password|authorization)\s*[=:]\s*[^\s,;)]+/gi;
|
|
12
|
+
const USER_TOKEN_PATTERN = /\b(?:user|cc)_[A-Za-z0-9_-]{8,}\b/gi;
|
|
13
|
+
const QUERY_SECRET_PATTERN = /([?&](?:api[-_ ]?key|apikey|access_token|refresh_token|token|secret|password)=)[^&#\s]+/gi;
|
|
14
|
+
const STANDALONE_SECRET_PATTERN = /\b(?:sk|rk|ghp|github_pat|xox[baprs])[-_A-Za-z0-9]{16,}\b|\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\b/g;
|
|
15
|
+
export function redactCommandCodeErrorText(value) {
|
|
16
|
+
return value
|
|
17
|
+
.replace(BEARER_PATTERN, "Bearer [redacted]")
|
|
18
|
+
.replace(CREDENTIAL_PATTERN, (match) => {
|
|
19
|
+
const separatorIndex = match.search(/[=:]/);
|
|
20
|
+
return separatorIndex < 0 ? "[redacted]" : `${match.slice(0, separatorIndex + 1)}[redacted]`;
|
|
21
|
+
})
|
|
22
|
+
.replace(USER_TOKEN_PATTERN, "[redacted]")
|
|
23
|
+
.replace(QUERY_SECRET_PATTERN, "$1[redacted]")
|
|
24
|
+
.replace(STANDALONE_SECRET_PATTERN, "[redacted]");
|
|
25
|
+
}
|
|
26
|
+
function isRecord(value) {
|
|
27
|
+
return typeof value === "object" && value !== null;
|
|
28
|
+
}
|
|
29
|
+
export function commandCodeErrorMessage(value) {
|
|
30
|
+
if (typeof value === "string")
|
|
31
|
+
return value;
|
|
32
|
+
if (!isRecord(value))
|
|
33
|
+
return undefined;
|
|
34
|
+
const record = value;
|
|
35
|
+
const parts = [];
|
|
36
|
+
for (const key of [
|
|
37
|
+
"message",
|
|
38
|
+
"errorMessage",
|
|
39
|
+
"error",
|
|
40
|
+
"detail",
|
|
41
|
+
"details",
|
|
42
|
+
"code",
|
|
43
|
+
"type",
|
|
44
|
+
"reason",
|
|
45
|
+
]) {
|
|
46
|
+
const part = commandCodeErrorMessage(record[key]);
|
|
47
|
+
if (part && !parts.includes(part))
|
|
48
|
+
parts.push(part);
|
|
49
|
+
}
|
|
50
|
+
for (const key of ["status", "statusCode", "httpStatus"]) {
|
|
51
|
+
const status = record[key];
|
|
52
|
+
if (typeof status === "string" || typeof status === "number") {
|
|
53
|
+
const statusPart = `status: ${status}`;
|
|
54
|
+
if (!parts.includes(statusPart))
|
|
55
|
+
parts.push(statusPart);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return parts.length > 0 ? redactCommandCodeErrorText(parts.join(": ")) : undefined;
|
|
59
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function isRetryableStatus(status: number): boolean;
|
|
2
|
+
export declare function parseRetryAfterSeconds(value: string | null): number | undefined;
|
|
3
|
+
export declare function retryDelayMs(attempt: number, retryAfterHeader: string | null, maxDelayMs: number): number;
|
|
4
|
+
export declare function abortError(message?: string): DOMException;
|
|
5
|
+
export declare function timeoutError(timeoutMs: number | undefined): Error;
|
|
6
|
+
export declare function raceAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T>;
|
|
7
|
+
export declare function raceAbortWithTimeout<T>(promise: Promise<T>, controller: AbortController, timeoutMs: number | undefined): Promise<T>;
|
|
8
|
+
export declare function delay(ms: number, signal: AbortSignal): Promise<void>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// src/provider/retry.ts — retry, abort and timeout helpers (PLAN #4, port of
|
|
2
|
+
// pi's core.ts retry math + abort races + signal-aware delay)
|
|
3
|
+
export function isRetryableStatus(status) {
|
|
4
|
+
return status === 429 || (status >= 500 && status < 600);
|
|
5
|
+
}
|
|
6
|
+
export function parseRetryAfterSeconds(value) {
|
|
7
|
+
if (!value)
|
|
8
|
+
return undefined;
|
|
9
|
+
const seconds = Number(value);
|
|
10
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
11
|
+
return seconds;
|
|
12
|
+
const date = Date.parse(value);
|
|
13
|
+
if (!Number.isNaN(date))
|
|
14
|
+
return Math.max(0, (date - Date.now()) / 1000);
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const BASE_RETRY_DELAY_MS = 500;
|
|
18
|
+
export function retryDelayMs(attempt, retryAfterHeader, maxDelayMs) {
|
|
19
|
+
const retryAfterMs = parseRetryAfterSeconds(retryAfterHeader);
|
|
20
|
+
if (retryAfterMs !== undefined) {
|
|
21
|
+
if (retryAfterMs * 1000 > maxDelayMs)
|
|
22
|
+
return -1;
|
|
23
|
+
return retryAfterMs * 1000;
|
|
24
|
+
}
|
|
25
|
+
const exponential = BASE_RETRY_DELAY_MS * 2 ** attempt;
|
|
26
|
+
const jitter = exponential * 0.2 * Math.random();
|
|
27
|
+
return Math.min(exponential + jitter, maxDelayMs);
|
|
28
|
+
}
|
|
29
|
+
export function abortError(message = "The operation was aborted") {
|
|
30
|
+
return new DOMException(message, "AbortError");
|
|
31
|
+
}
|
|
32
|
+
export function timeoutError(timeoutMs) {
|
|
33
|
+
return new Error(timeoutMs === undefined
|
|
34
|
+
? "Command Code API request timed out"
|
|
35
|
+
: `Command Code API request timed out after ${timeoutMs}ms`);
|
|
36
|
+
}
|
|
37
|
+
export function raceAbort(promise, signal) {
|
|
38
|
+
if (signal.aborted)
|
|
39
|
+
return Promise.reject(abortError());
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const onAbort = () => reject(abortError());
|
|
42
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
43
|
+
promise.then((value) => {
|
|
44
|
+
signal.removeEventListener("abort", onAbort);
|
|
45
|
+
resolve(value);
|
|
46
|
+
}, (error) => {
|
|
47
|
+
signal.removeEventListener("abort", onAbort);
|
|
48
|
+
reject(error);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export function raceAbortWithTimeout(promise, controller, timeoutMs) {
|
|
53
|
+
if (timeoutMs === undefined)
|
|
54
|
+
return raceAbort(promise, controller.signal);
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const timer = setTimeout(() => {
|
|
57
|
+
controller.abort();
|
|
58
|
+
reject(timeoutError(timeoutMs));
|
|
59
|
+
}, timeoutMs);
|
|
60
|
+
raceAbort(promise, controller.signal).then((value) => {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
resolve(value);
|
|
63
|
+
}, (error) => {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
reject(error);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export function delay(ms, signal) {
|
|
70
|
+
if (signal.aborted)
|
|
71
|
+
return Promise.reject(abortError());
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const onAbort = () => {
|
|
74
|
+
clearTimeout(id);
|
|
75
|
+
reject(abortError());
|
|
76
|
+
};
|
|
77
|
+
const id = setTimeout(() => {
|
|
78
|
+
signal.removeEventListener("abort", onAbort);
|
|
79
|
+
resolve();
|
|
80
|
+
}, ms);
|
|
81
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LanguageModelV3StreamPart, LanguageModelV3Usage, LanguageModelV3FinishReason } from "@ai-sdk/provider";
|
|
2
|
+
export declare function parseStreamEventLine(line: string): unknown | undefined;
|
|
3
|
+
export declare function mapFinishReason(reason: unknown): LanguageModelV3FinishReason;
|
|
4
|
+
export declare function ccUsageToAiSdkUsage(event: Record<string, unknown>): LanguageModelV3Usage | undefined;
|
|
5
|
+
export declare function ccEventToStreamPart(event: unknown): LanguageModelV3StreamPart[];
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { isRecord, stringValue, numberValue, recordOrEmpty } from "./converters.js";
|
|
2
|
+
import { commandCodeErrorMessage } from "./redact.js";
|
|
3
|
+
export function parseStreamEventLine(line) {
|
|
4
|
+
let trimmed = line.trim();
|
|
5
|
+
if (!trimmed || trimmed.startsWith(":") || trimmed.startsWith("event:"))
|
|
6
|
+
return undefined;
|
|
7
|
+
if (trimmed.startsWith("data:"))
|
|
8
|
+
trimmed = trimmed.slice(5).trim();
|
|
9
|
+
if (!trimmed || trimmed === "[DONE]")
|
|
10
|
+
return undefined;
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(trimmed);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function mapFinishReason(reason) {
|
|
19
|
+
const raw = stringValue(reason) ?? "unknown";
|
|
20
|
+
if (raw === "tool_use" || raw === "tool-calls")
|
|
21
|
+
return { unified: "tool-calls", raw };
|
|
22
|
+
if (raw === "length" ||
|
|
23
|
+
raw === "max_tokens" ||
|
|
24
|
+
raw === "max-tokens" ||
|
|
25
|
+
raw === "max_output_tokens") {
|
|
26
|
+
return { unified: "length", raw };
|
|
27
|
+
}
|
|
28
|
+
if (raw === "stop")
|
|
29
|
+
return { unified: "stop", raw };
|
|
30
|
+
if (raw === "error")
|
|
31
|
+
return { unified: "error", raw };
|
|
32
|
+
if (raw === "content-filter")
|
|
33
|
+
return { unified: "content-filter", raw };
|
|
34
|
+
return { unified: "other", raw };
|
|
35
|
+
}
|
|
36
|
+
export function ccUsageToAiSdkUsage(event) {
|
|
37
|
+
const usage = event.totalUsage;
|
|
38
|
+
if (!isRecord(usage))
|
|
39
|
+
return undefined;
|
|
40
|
+
const details = isRecord(usage.inputTokenDetails) ? usage.inputTokenDetails : undefined;
|
|
41
|
+
const totalInput = numberValue(usage.inputTokens) ?? 0;
|
|
42
|
+
const noCache = numberValue(details?.noCacheTokens);
|
|
43
|
+
const cacheRead = numberValue(details?.cacheReadTokens) ?? 0;
|
|
44
|
+
const cacheWrite = numberValue(details?.cacheWriteTokens) ?? 0;
|
|
45
|
+
const inputTotal = noCache ?? Math.max(0, totalInput - cacheRead - cacheWrite);
|
|
46
|
+
const outputTokens = numberValue(usage.outputTokens) ?? 0;
|
|
47
|
+
return {
|
|
48
|
+
inputTokens: { total: inputTotal, noCache: inputTotal, cacheRead, cacheWrite },
|
|
49
|
+
outputTokens: { total: outputTokens, text: outputTokens, reasoning: 0 },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function toolCallIdOf(event) {
|
|
53
|
+
return stringValue(event.toolCallId) ?? stringValue(event.id) ?? "";
|
|
54
|
+
}
|
|
55
|
+
export function ccEventToStreamPart(event) {
|
|
56
|
+
if (!isRecord(event))
|
|
57
|
+
return [];
|
|
58
|
+
switch (event.type) {
|
|
59
|
+
case "text-delta": {
|
|
60
|
+
const id = toolCallIdOf(event) || "text";
|
|
61
|
+
return [{ type: "text-delta", id, delta: stringValue(event.text) ?? "" }];
|
|
62
|
+
}
|
|
63
|
+
case "reasoning-delta": {
|
|
64
|
+
const id = toolCallIdOf(event) || "reasoning";
|
|
65
|
+
return [{ type: "reasoning-delta", id, delta: stringValue(event.text) ?? "" }];
|
|
66
|
+
}
|
|
67
|
+
case "reasoning-start":
|
|
68
|
+
case "reasoning-end":
|
|
69
|
+
case "tool-result":
|
|
70
|
+
return [];
|
|
71
|
+
case "tool-call": {
|
|
72
|
+
const id = toolCallIdOf(event);
|
|
73
|
+
const toolName = stringValue(event.toolName) ?? "";
|
|
74
|
+
const args = recordOrEmpty(event.input ?? event.args ?? event.arguments);
|
|
75
|
+
const argsTextDelta = JSON.stringify(args);
|
|
76
|
+
return [
|
|
77
|
+
{ type: "tool-input-start", id, toolName },
|
|
78
|
+
{ type: "tool-input-delta", id, delta: argsTextDelta },
|
|
79
|
+
{ type: "tool-input-end", id },
|
|
80
|
+
{ type: "tool-call", toolCallId: id, toolName, input: argsTextDelta },
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
case "finish": {
|
|
84
|
+
const usage = ccUsageToAiSdkUsage(event);
|
|
85
|
+
return [
|
|
86
|
+
{
|
|
87
|
+
type: "finish",
|
|
88
|
+
finishReason: mapFinishReason(event.finishReason),
|
|
89
|
+
usage: usage ?? {
|
|
90
|
+
inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },
|
|
91
|
+
outputTokens: { total: 0, text: 0, reasoning: 0 },
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
case "error": {
|
|
97
|
+
const message = commandCodeErrorMessage(event.error) ??
|
|
98
|
+
commandCodeErrorMessage(event.message) ??
|
|
99
|
+
"Command Code stream error";
|
|
100
|
+
throw new Error(message);
|
|
101
|
+
}
|
|
102
|
+
default:
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-cmd-provider",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Command Code provider + plugin for opencode",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js",
|
|
9
|
+
"./server": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"CHANGELOG.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.json",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"test": "npm run typecheck && npm run test:unit && npm run test:integration && npm run test:contract && npm run format:check",
|
|
21
|
+
"test:unit": "tsx tests/env.test.ts && tsx tests/auth-key.test.ts && tsx tests/converters.test.ts && tsx tests/stream.test.ts && tsx tests/redact.test.ts && tsx tests/cost.test.ts && tsx tests/retry.test.ts && tsx tests/reasoning.test.ts && tsx tests/modalities.test.ts && tsx tests/models.test.ts && tsx tests/oauth.test.ts && tsx tests/plugin-models.test.ts",
|
|
22
|
+
"test:integration": "tsx tests/integration-do-stream.test.ts && tsx tests/integration-do-generate.test.ts",
|
|
23
|
+
"test:contract": "tsx tests/contract.test.ts",
|
|
24
|
+
"test:e2e": "node tests/e2e-opencode.mjs",
|
|
25
|
+
"format": "prettier --write '**/*.{ts,mjs,json,md}'",
|
|
26
|
+
"format:check": "prettier --check '**/*.{ts,mjs,json,md}'"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"opencode",
|
|
30
|
+
"opencode-plugin",
|
|
31
|
+
"commandcode",
|
|
32
|
+
"provider",
|
|
33
|
+
"ai-sdk"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/rashidrazak/opencode-cmd-provider.git"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@ai-sdk/provider": "^3.0.8"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@opencode-ai/plugin": "*",
|
|
45
|
+
"@types/node": "^25.9.5",
|
|
46
|
+
"prettier": "^3.9.6",
|
|
47
|
+
"tsx": "^4.23.12",
|
|
48
|
+
"typescript": "^7.0.2"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@opencode-ai/plugin": "*"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"@opencode-ai/plugin": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|