@andershaf/testopencode 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/README.md +54 -0
- package/dist/provider.d.ts +8 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +186 -0
- package/dist/types.d.ts +25 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# @andershaf/testopencode
|
|
2
|
+
|
|
3
|
+
OpenCode custom provider for **CDF project AI** (`/api/v1/projects/{project}/ai/chat/completions`).
|
|
4
|
+
|
|
5
|
+
- Strips fields CDF rejects (`max_tokens`, `tool_choice`, `stream_options`, tool `parameters.$schema`, …).
|
|
6
|
+
- Normalizes **CDF camelCase** responses to **OpenAI-style snake_case** for the AI SDK.
|
|
7
|
+
|
|
8
|
+
Derived from [cognitedata/atlascode PR #1](https://github.com/cognitedata/atlascode/pull/1).
|
|
9
|
+
|
|
10
|
+
## OpenCode `opencode.json`
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"provider": {
|
|
15
|
+
"cdf": {
|
|
16
|
+
"name": "CDF",
|
|
17
|
+
"npm": "@andershaf/testopencode",
|
|
18
|
+
"models": {
|
|
19
|
+
"azure/gpt-5.1": {
|
|
20
|
+
"name": "GPT-5.1",
|
|
21
|
+
"id": "azure/gpt-5.1",
|
|
22
|
+
"tool_call": true
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"options": {
|
|
26
|
+
"cluster": "api",
|
|
27
|
+
"project": "your-project",
|
|
28
|
+
"token": "{env:CDF_TOKEN}",
|
|
29
|
+
"headers": {
|
|
30
|
+
"cdf-version": "20230101-alpha"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"model": "cdf/azure/gpt-5.1"
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Publish
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cd packages/testopencode
|
|
43
|
+
npm install
|
|
44
|
+
npm run build
|
|
45
|
+
npm publish --access public
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Use an npm account with access to the `@andershaf` scope.
|
|
49
|
+
|
|
50
|
+
## Develop
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm run build
|
|
54
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CdfProviderOptions } from "./types.js";
|
|
2
|
+
export declare function sanitizeRequestBody(body: Record<string, any>): {
|
|
3
|
+
[x: string]: any;
|
|
4
|
+
};
|
|
5
|
+
export declare function normalizeCdfPayload(value: any): any;
|
|
6
|
+
export declare function createCdfProvider(options: CdfProviderOptions): import("@ai-sdk/openai-compatible").OpenAICompatibleProvider<string, string, string, string>;
|
|
7
|
+
export default createCdfProvider;
|
|
8
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAsCpD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;;EAsC5D;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,CAenD;AAgGD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,gGAoC5D;AAED,eAAe,iBAAiB,CAAA"}
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDF Atlas-style OpenCode provider: sanitize outbound bodies for strict CDF AI,
|
|
3
|
+
* normalize camelCase responses for OpenCode / AI SDK.
|
|
4
|
+
*
|
|
5
|
+
* Based on cognitedata/atlascode (PR #1) + cdf-version header merge.
|
|
6
|
+
* @see https://github.com/cognitedata/atlascode/pull/1
|
|
7
|
+
*/
|
|
8
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
9
|
+
const CDF_TO_OPENAI_KEY_MAP = {
|
|
10
|
+
acceptedPredictionTokens: "accepted_prediction_tokens",
|
|
11
|
+
cachedTokens: "cached_tokens",
|
|
12
|
+
completionTokens: "completion_tokens",
|
|
13
|
+
completionTokensDetails: "completion_tokens_details",
|
|
14
|
+
extraContent: "extra_content",
|
|
15
|
+
finishReason: "finish_reason",
|
|
16
|
+
promptTokens: "prompt_tokens",
|
|
17
|
+
promptTokensDetails: "prompt_tokens_details",
|
|
18
|
+
reasoningContent: "reasoning_content",
|
|
19
|
+
reasoningTokens: "reasoning_tokens",
|
|
20
|
+
rejectedPredictionTokens: "rejected_prediction_tokens",
|
|
21
|
+
thoughtSignature: "thought_signature",
|
|
22
|
+
toolCalls: "tool_calls",
|
|
23
|
+
totalTokens: "total_tokens",
|
|
24
|
+
};
|
|
25
|
+
const CDF_MISSING_TOKEN_MESSAGE = "CDF provider is missing an access token. Set CDF_TOKEN in your environment and restart OpenCode, or configure provider.options.token / an Authorization: Bearer ... header for the cdf provider.";
|
|
26
|
+
function stripUnsupportedSchemaFields(value) {
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
return value.map(stripUnsupportedSchemaFields);
|
|
29
|
+
}
|
|
30
|
+
if (value && typeof value === "object") {
|
|
31
|
+
const entries = Object.entries(value)
|
|
32
|
+
.filter(([key]) => key !== "$schema" && key !== "ref")
|
|
33
|
+
.map(([key, nested]) => [key, stripUnsupportedSchemaFields(nested)]);
|
|
34
|
+
return Object.fromEntries(entries);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
export function sanitizeRequestBody(body) {
|
|
39
|
+
const { max_tokens, stream_options, tool_choice, reasoningSummary, reasoning_effort, top_p, verbosity, ...rest } = body;
|
|
40
|
+
if (rest.tools) {
|
|
41
|
+
rest.tools = rest.tools.map((tool) => {
|
|
42
|
+
if (!tool.function?.parameters)
|
|
43
|
+
return tool;
|
|
44
|
+
return {
|
|
45
|
+
...tool,
|
|
46
|
+
function: {
|
|
47
|
+
...tool.function,
|
|
48
|
+
parameters: stripUnsupportedSchemaFields(tool.function.parameters),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (rest.messages) {
|
|
54
|
+
rest.messages = rest.messages.map(({ cache_control, reasoning_content, ...msg }) => {
|
|
55
|
+
if (Array.isArray(msg.content)) {
|
|
56
|
+
msg.content = msg.content.map(({ cache_control: _, ...part }) => part);
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(msg.tool_calls)) {
|
|
59
|
+
msg.tool_calls = msg.tool_calls.map(({ cache_control: _, ...toolCall }) => toolCall);
|
|
60
|
+
}
|
|
61
|
+
return msg;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return rest;
|
|
65
|
+
}
|
|
66
|
+
export function normalizeCdfPayload(value) {
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
return value.map(normalizeCdfPayload);
|
|
69
|
+
}
|
|
70
|
+
if (value && typeof value === "object") {
|
|
71
|
+
return Object.fromEntries(Object.entries(value).map(([key, nested]) => [
|
|
72
|
+
CDF_TO_OPENAI_KEY_MAP[key] ?? key,
|
|
73
|
+
normalizeCdfPayload(nested),
|
|
74
|
+
]));
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
function copyResponseHeaders(response, contentType) {
|
|
79
|
+
const headers = new Headers(response.headers);
|
|
80
|
+
headers.delete("content-length");
|
|
81
|
+
if (contentType)
|
|
82
|
+
headers.set("content-type", contentType);
|
|
83
|
+
return headers;
|
|
84
|
+
}
|
|
85
|
+
function transformJsonlToEventStream(stream) {
|
|
86
|
+
const encoder = new TextEncoder();
|
|
87
|
+
let buffer = "";
|
|
88
|
+
return stream
|
|
89
|
+
.pipeThrough(new TextDecoderStream())
|
|
90
|
+
.pipeThrough(new TransformStream({
|
|
91
|
+
transform(chunk, controller) {
|
|
92
|
+
buffer += chunk;
|
|
93
|
+
const lines = buffer.split(/\r?\n/);
|
|
94
|
+
buffer = lines.pop() ?? "";
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
const trimmed = line.trim();
|
|
97
|
+
if (!trimmed)
|
|
98
|
+
continue;
|
|
99
|
+
const normalized = normalizeCdfPayload(JSON.parse(trimmed));
|
|
100
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(normalized)}\n\n`));
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
flush(controller) {
|
|
104
|
+
const trimmed = buffer.trim();
|
|
105
|
+
if (!trimmed)
|
|
106
|
+
return;
|
|
107
|
+
const normalized = normalizeCdfPayload(JSON.parse(trimmed));
|
|
108
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(normalized)}\n\n`));
|
|
109
|
+
},
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
async function normalizeCdfResponse(response) {
|
|
113
|
+
if (!response.ok)
|
|
114
|
+
return response;
|
|
115
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
116
|
+
if (contentType.includes("application/jsonl") && response.body) {
|
|
117
|
+
return new Response(transformJsonlToEventStream(response.body), {
|
|
118
|
+
status: response.status,
|
|
119
|
+
statusText: response.statusText,
|
|
120
|
+
headers: copyResponseHeaders(response, "text/event-stream"),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (!contentType.includes("application/json")) {
|
|
124
|
+
return response;
|
|
125
|
+
}
|
|
126
|
+
const text = await response.text();
|
|
127
|
+
const normalized = normalizeCdfPayload(JSON.parse(text));
|
|
128
|
+
return new Response(JSON.stringify(normalized), {
|
|
129
|
+
status: response.status,
|
|
130
|
+
statusText: response.statusText,
|
|
131
|
+
headers: copyResponseHeaders(response, "application/json"),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function getBearerToken(headers) {
|
|
135
|
+
if (!headers)
|
|
136
|
+
return undefined;
|
|
137
|
+
const value = new Headers(headers).get("Authorization");
|
|
138
|
+
if (!value)
|
|
139
|
+
return undefined;
|
|
140
|
+
const match = /^Bearer\s+(.+)$/i.exec(value);
|
|
141
|
+
return match?.[1];
|
|
142
|
+
}
|
|
143
|
+
function createMissingTokenResponse() {
|
|
144
|
+
return new Response(JSON.stringify({
|
|
145
|
+
error: {
|
|
146
|
+
message: CDF_MISSING_TOKEN_MESSAGE,
|
|
147
|
+
code: "missing_token",
|
|
148
|
+
},
|
|
149
|
+
}), {
|
|
150
|
+
status: 401,
|
|
151
|
+
headers: {
|
|
152
|
+
"content-type": "application/json",
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
export function createCdfProvider(options) {
|
|
157
|
+
const { cluster, project, baseURL, name = "cdf", token, headers: extraHeaders } = options;
|
|
158
|
+
const staticToken = token ?? getBearerToken(extraHeaders);
|
|
159
|
+
const resolvedBaseURL = baseURL ?? `https://${cluster}.cognitedata.com/api/v1/projects/${project}/ai`;
|
|
160
|
+
return createOpenAICompatible({
|
|
161
|
+
name,
|
|
162
|
+
apiKey: "cdf",
|
|
163
|
+
baseURL: resolvedBaseURL,
|
|
164
|
+
fetch: (async (input, init) => {
|
|
165
|
+
if (!staticToken) {
|
|
166
|
+
return createMissingTokenResponse();
|
|
167
|
+
}
|
|
168
|
+
const headers = new Headers(init?.headers);
|
|
169
|
+
if (extraHeaders) {
|
|
170
|
+
new Headers(extraHeaders).forEach((value, key) => {
|
|
171
|
+
if (key.toLowerCase() !== "authorization") {
|
|
172
|
+
headers.set(key, value);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
headers.set("Authorization", `Bearer ${staticToken}`);
|
|
177
|
+
let body = init?.body;
|
|
178
|
+
if (typeof body === "string") {
|
|
179
|
+
body = JSON.stringify(sanitizeRequestBody(JSON.parse(body)));
|
|
180
|
+
}
|
|
181
|
+
const response = await globalThis.fetch(input, { ...init, headers, body });
|
|
182
|
+
return normalizeCdfResponse(response);
|
|
183
|
+
}),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
export default createCdfProvider;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Based on cognitedata/atlascode (PR #1).
|
|
3
|
+
* @see https://github.com/cognitedata/atlascode/pull/1
|
|
4
|
+
*/
|
|
5
|
+
export interface CdfProviderOptions {
|
|
6
|
+
/** CDF cluster (e.g. "api", "westeurope-1"). Required unless baseURL is set. */
|
|
7
|
+
cluster: string;
|
|
8
|
+
/** CDF project name. Required unless baseURL is set. */
|
|
9
|
+
project: string;
|
|
10
|
+
/** OAuth access token for CDF API calls. */
|
|
11
|
+
token?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Extra headers (e.g. cdf-version). If Authorization is set, bearer is used as token.
|
|
14
|
+
*/
|
|
15
|
+
headers?: HeadersInit;
|
|
16
|
+
/**
|
|
17
|
+
* Chat API base URL.
|
|
18
|
+
* @default https://${cluster}.cognitedata.com/api/v1/projects/${project}/ai
|
|
19
|
+
*/
|
|
20
|
+
baseURL?: string;
|
|
21
|
+
/** Provider display name in OpenCode. @default "cdf" */
|
|
22
|
+
name?: string;
|
|
23
|
+
defaultModel?: string;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,gFAAgF;IAChF,OAAO,EAAE,MAAM,CAAA;IAEf,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAA;IAEf,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd;;OAEG;IACH,OAAO,CAAC,EAAE,WAAW,CAAA;IAErB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB,wDAAwD;IACxD,IAAI,CAAC,EAAE,MAAM,CAAA;IAEb,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@andershaf/testopencode",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode provider for CDF project AI chat (atlascode-style request sanitize + response normalize)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/provider.js",
|
|
7
|
+
"types": "./dist/provider.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/provider.d.ts",
|
|
11
|
+
"import": "./dist/provider.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"opencode",
|
|
23
|
+
"cognite",
|
|
24
|
+
"cdf",
|
|
25
|
+
"ai-sdk"
|
|
26
|
+
],
|
|
27
|
+
"license": "Apache-2.0",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/cognitedata/atlascode.git"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@ai-sdk/openai-compatible": "^1.0.34",
|
|
34
|
+
"@ai-sdk/provider-utils": "^3.0.22"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@ai-sdk/provider": "^2.0.0",
|
|
38
|
+
"zod": "^3.25.76 || ^4.1.8"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"typescript": "^5.8.0"
|
|
42
|
+
}
|
|
43
|
+
}
|