Merge pull request #2270 from pikasTech/issue-2268-opencode-agentrun-provider
OpenCode阶段一接入dsflash-go provider配置
This commit is contained in:
@@ -337,6 +337,7 @@
|
||||
"imageTagMode": { "type": "string", "enum": ["short", "full"] },
|
||||
"workbench": { "$ref": "#/$defs/workbenchUiPolicy" },
|
||||
"observability": { "$ref": "#/$defs/runtimeObservabilityPolicy" },
|
||||
"opencode": { "$ref": "#/$defs/opencodeRuntime" },
|
||||
"runtimeStore": { "$ref": "#/$defs/runtimeStore" },
|
||||
"workbenchRuntime": { "$ref": "#/$defs/workbenchRuntime" },
|
||||
"sourceRepo": { "type": "string" },
|
||||
@@ -368,6 +369,30 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"opencodeRuntime": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"providerProfile": { "type": "string", "minLength": 1 },
|
||||
"providerId": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$" },
|
||||
"displayName": { "type": "string", "minLength": 1 },
|
||||
"model": { "type": "string", "minLength": 1 },
|
||||
"smallModel": { "type": "string", "minLength": 1 },
|
||||
"baseURL": { "type": "string", "minLength": 1 },
|
||||
"npm": { "type": "string", "minLength": 1 },
|
||||
"apiKeyEnv": { "type": "string", "pattern": "^[A-Z0-9_]+$" },
|
||||
"apiKeySecretRef": { "type": "string", "pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$" },
|
||||
"contextLimit": { "type": "integer", "minimum": 1 },
|
||||
"outputLimit": { "type": "integer", "minimum": 1 },
|
||||
"agentrunSecretNamespace": { "type": "string", "minLength": 1 },
|
||||
"agentrunSecretName": { "type": "string", "minLength": 1 },
|
||||
"agentrunSecretKeys": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"uniqueItems": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"runtimeObservabilityPolicy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -335,6 +335,24 @@ lanes:
|
||||
autoCollapseTerminal: false
|
||||
observability:
|
||||
traceExplorerUrlTemplate: /v1/workbench/traces/{trace_id}/events
|
||||
opencode:
|
||||
providerProfile: dsflash-go
|
||||
providerId: dsflash-go
|
||||
displayName: AgentRun dsflash-go
|
||||
model: deepseek-v4-flash
|
||||
smallModel: deepseek-v4-flash
|
||||
baseURL: https://opencode.ai/zen/go/v1
|
||||
npm: '@ai-sdk/openai-compatible'
|
||||
apiKeyEnv: OPENCODE_DSFLASH_GO_API_KEY
|
||||
apiKeySecretRef: hwlab-code-agent-provider/opencode-api-key
|
||||
contextLimit: 1000000
|
||||
outputLimit: 384000
|
||||
agentrunSecretNamespace: agentrun-v02
|
||||
agentrunSecretName: agentrun-v01-provider-dsflash-go
|
||||
agentrunSecretKeys:
|
||||
- auth.json
|
||||
- config.toml
|
||||
- model-catalog.json
|
||||
externalPostgres:
|
||||
enabled: true
|
||||
serviceName: g14-platform-postgres
|
||||
|
||||
+104
-18
@@ -415,6 +415,10 @@ function requiredAccessConfigString(value, label) {
|
||||
return text;
|
||||
}
|
||||
|
||||
function optionalObject(value) {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
||||
}
|
||||
|
||||
async function gitValue(args) {
|
||||
const result = await execFileAsync("git", args, { cwd: repoRoot, timeout: 10000, maxBuffer: 1024 * 1024 });
|
||||
return result.stdout.trim();
|
||||
@@ -987,6 +991,13 @@ function secretNameForProfile(secretName, profile) {
|
||||
return secretName;
|
||||
}
|
||||
|
||||
function secretRefObjectForProfile(secretRef, profile) {
|
||||
const ref = configString(secretRef);
|
||||
const slashIndex = ref.indexOf("/");
|
||||
assert.ok(slashIndex > 0 && slashIndex < ref.length - 1, `secretRef ${JSON.stringify(secretRef)} must be name/key`);
|
||||
return { name: secretNameForProfile(ref.slice(0, slashIndex), profile), key: ref.slice(slashIndex + 1), optional: true };
|
||||
}
|
||||
|
||||
function rewritePodSecretRefs(podSpec, profile) {
|
||||
if (!podSpec || !isRuntimeLane(profile)) return;
|
||||
for (const volume of podSpec.volumes ?? []) {
|
||||
@@ -996,20 +1007,7 @@ function rewritePodSecretRefs(podSpec, profile) {
|
||||
|
||||
function deployEnvEntry(name, value, namespace, profile = "dev") {
|
||||
if (typeof value === "string" && value.startsWith("secretRef:")) {
|
||||
const ref = value.slice("secretRef:".length);
|
||||
const slashIndex = ref.indexOf("/");
|
||||
if (slashIndex > 0 && slashIndex < ref.length - 1) {
|
||||
return {
|
||||
name,
|
||||
valueFrom: {
|
||||
secretKeyRef: {
|
||||
name: secretNameForProfile(ref.slice(0, slashIndex), profile),
|
||||
key: ref.slice(slashIndex + 1),
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return { name, valueFrom: { secretKeyRef: secretRefObjectForProfile(value.slice("secretRef:".length), profile) } };
|
||||
}
|
||||
return { name, value: namespaceScopedHost(value, namespace) };
|
||||
}
|
||||
@@ -5638,7 +5636,81 @@ function secretPlaneIssueLabelValue(issueRef) {
|
||||
return label;
|
||||
}
|
||||
|
||||
function opencodeServerManifest({ profile = "v03", source }) {
|
||||
function opencodeRuntimeConfigForProfile(deploy, profile) {
|
||||
const config = optionalObject(deploy?.lanes?.[profile]?.opencode);
|
||||
const providerProfile = configString(config.providerProfile) || "dsflash-go";
|
||||
const providerId = configString(config.providerId) || providerProfile;
|
||||
const model = configString(config.model) || "deepseek-v4-flash";
|
||||
const smallModel = configString(config.smallModel) || model;
|
||||
const apiKeyEnv = configString(config.apiKeyEnv) || opencodeApiKeyEnvName(providerId);
|
||||
const contextLimit = opencodePositiveInteger(config.contextLimit, 1000000, `deploy.lanes.${profile}.opencode.contextLimit`);
|
||||
const outputLimit = opencodePositiveInteger(config.outputLimit, 384000, `deploy.lanes.${profile}.opencode.outputLimit`);
|
||||
return {
|
||||
providerProfile,
|
||||
providerId,
|
||||
displayName: configString(config.displayName) || `AgentRun ${providerProfile}`,
|
||||
model,
|
||||
smallModel,
|
||||
baseURL: configString(config.baseURL) || "https://opencode.ai/zen/go/v1",
|
||||
npm: configString(config.npm) || "@ai-sdk/openai-compatible",
|
||||
apiKeyEnv,
|
||||
apiKeySecretRef: configString(config.apiKeySecretRef) || "hwlab-code-agent-provider/opencode-api-key",
|
||||
contextLimit,
|
||||
outputLimit,
|
||||
agentrunSecretNamespace: configString(config.agentrunSecretNamespace) || "agentrun-v02",
|
||||
agentrunSecretName: configString(config.agentrunSecretName) || `agentrun-v01-provider-${providerProfile}`,
|
||||
agentrunSecretKeys: Array.isArray(config.agentrunSecretKeys) && config.agentrunSecretKeys.length > 0
|
||||
? config.agentrunSecretKeys.map(configString).filter(Boolean)
|
||||
: ["auth.json", "config.toml", "model-catalog.json"]
|
||||
};
|
||||
}
|
||||
|
||||
function opencodePositiveInteger(value, fallback, label) {
|
||||
if (value === undefined || value === null) return fallback;
|
||||
assert.ok(Number.isInteger(value) && value > 0, `${label} must be a positive integer`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function opencodeApiKeyEnvName(providerId) {
|
||||
const normalized = String(providerId || "provider").toUpperCase().replace(/[^A-Z0-9]+/gu, "_").replace(/^_+|_+$/gu, "");
|
||||
return `OPENCODE_${normalized || "PROVIDER"}_API_KEY`;
|
||||
}
|
||||
|
||||
function opencodeConfigContent(config) {
|
||||
return JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
autoupdate: false,
|
||||
share: "disabled",
|
||||
model: `${config.providerId}/${config.model}`,
|
||||
small_model: `${config.providerId}/${config.smallModel}`,
|
||||
provider: {
|
||||
[config.providerId]: {
|
||||
npm: config.npm,
|
||||
name: config.displayName,
|
||||
options: {
|
||||
baseURL: config.baseURL,
|
||||
apiKey: `{env:${config.apiKeyEnv}}`,
|
||||
timeout: 600000,
|
||||
headerTimeout: 600000,
|
||||
chunkTimeout: 300000
|
||||
},
|
||||
models: {
|
||||
[config.model]: {
|
||||
name: config.model,
|
||||
reasoning: true,
|
||||
tool_call: true,
|
||||
limit: {
|
||||
context: config.contextLimit,
|
||||
output: config.outputLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function opencodeServerManifest({ profile = "v03", source, deploy = null }) {
|
||||
assert.ok(isRuntimeLane(profile), `opencode-server profile must be a runtime lane, got ${profile}`);
|
||||
const namespace = namespaceNameForProfile(profile);
|
||||
const name = "opencode-server";
|
||||
@@ -5655,9 +5727,22 @@ function opencodeServerManifest({ profile = "v03", source }) {
|
||||
"hwlab.pikastech.local/service-id": name,
|
||||
"hwlab.pikastech.local/source-commit": source.full
|
||||
};
|
||||
const annotations = { "hwlab.pikastech.local/rendered-by": "scripts/gitops-render.mjs" };
|
||||
const opencodeConfig = opencodeRuntimeConfigForProfile(deploy, profile);
|
||||
const configContent = opencodeConfigContent(opencodeConfig);
|
||||
const configSha256 = createHash("sha256").update(configContent).digest("hex");
|
||||
const apiKeySecretRef = secretRefObjectForProfile(opencodeConfig.apiKeySecretRef, profile);
|
||||
const annotations = {
|
||||
"hwlab.pikastech.local/rendered-by": "scripts/gitops-render.mjs",
|
||||
"hwlab.pikastech.local/opencode-provider-profile": opencodeConfig.providerProfile,
|
||||
"hwlab.pikastech.local/opencode-provider-id": opencodeConfig.providerId,
|
||||
"hwlab.pikastech.local/opencode-model": opencodeConfig.model,
|
||||
"hwlab.pikastech.local/opencode-config-sha256": configSha256,
|
||||
"hwlab.pikastech.local/opencode-api-key-secret-ref": `${apiKeySecretRef.name}/${apiKeySecretRef.key}`,
|
||||
"hwlab.pikastech.local/agentrun-provider-secret-ref": `${opencodeConfig.agentrunSecretNamespace}/${opencodeConfig.agentrunSecretName}`,
|
||||
"hwlab.pikastech.local/agentrun-provider-secret-keys": opencodeConfig.agentrunSecretKeys.join(","),
|
||||
"hwlab.pikastech.local/values-printed": "false"
|
||||
};
|
||||
const authSecretName = secretNameForProfile("hwlab-opencode-server-auth", profile);
|
||||
const configContent = JSON.stringify({ autoupdate: false, share: "disabled" });
|
||||
return {
|
||||
apiVersion: "v1",
|
||||
kind: "List",
|
||||
@@ -5711,6 +5796,7 @@ function opencodeServerManifest({ profile = "v03", source }) {
|
||||
{ name: "XDG_CONFIG_HOME", value: "/workspace/.config" },
|
||||
{ name: "XDG_DATA_HOME", value: "/workspace/.local/share" },
|
||||
{ name: "OPENCODE_CONFIG_CONTENT", value: configContent },
|
||||
{ name: opencodeConfig.apiKeyEnv, valueFrom: { secretKeyRef: apiKeySecretRef } },
|
||||
{ name: "OPENCODE_SERVER_USERNAME", valueFrom: { secretKeyRef: { name: authSecretName, key: "username" } } },
|
||||
{ name: "OPENCODE_SERVER_PASSWORD", valueFrom: { secretKeyRef: { name: authSecretName, key: "password" } } }
|
||||
],
|
||||
@@ -5905,7 +5991,7 @@ async function plannedFiles(args) {
|
||||
if (workbenchRedis) putJson(`${runtimePath}/workbench-redis.yaml`, workbenchRuntimeRedisManifest({ profile, config: workbenchRedis, source }));
|
||||
if (isRuntimeLane(profile)) putJson(`${runtimePath}/openfga.yaml`, v02OpenFgaManifest({ profile, source }));
|
||||
if (isRuntimeLane(profile)) putJson(`${runtimePath}/observability.yaml`, observabilityManifest({ deploy, profile, namespace, labels: profileLabels, annotations, metricsSidecarScript }));
|
||||
if (isRuntimeLane(profile)) putJson(`${runtimePath}/opencode.yaml`, opencodeServerManifest({ profile, source }));
|
||||
if (isRuntimeLane(profile)) putJson(`${runtimePath}/opencode.yaml`, opencodeServerManifest({ profile, source, deploy }));
|
||||
if (includeDeviceAgent) putJson(`${runtimePath}/device-agent-71-freq.yaml`, deviceAgent71FreqManifest({ profile, source, registryPrefix: args.registryPrefix, catalog: artifactCatalog, useDeployImages: args.useDeployImages }));
|
||||
putJson(`${runtimePath}/node-frpc.yaml`, nodeFrpcManifest({ profile, source }));
|
||||
}
|
||||
|
||||
@@ -691,6 +691,7 @@ test("v03 render includes D518 secret-plane smoke on D518 gitops root", async ()
|
||||
const generatedPaths = generatedFiles.map((filePath) => path.relative(outDir, filePath));
|
||||
assert.ok(generatedPaths.includes("runtime-v03/kustomization.yaml"));
|
||||
assert.ok(generatedPaths.includes("runtime-v03/workloads.yaml"));
|
||||
assert.ok(generatedPaths.includes("runtime-v03/opencode.yaml"));
|
||||
assert.equal(generatedPaths.includes("runtime-v03/external-secrets.yaml"), true);
|
||||
const kustomization = await readFile(path.join(outDir, "runtime-v03", "kustomization.yaml"), "utf8");
|
||||
assert.match(kustomization, /external-secrets\.yaml/u);
|
||||
@@ -716,6 +717,24 @@ test("v03 render includes D518 secret-plane smoke on D518 gitops root", async ()
|
||||
.filter((entry) => entry.name === "HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID");
|
||||
assert.ok(agentRunNodeEnv.length > 0, "expected D518 node id to be inferred from gitops root");
|
||||
assert.deepEqual([...new Set(agentRunNodeEnv.map((entry) => entry.value))], ["D518"]);
|
||||
|
||||
const opencode = JSON.parse(await readFile(path.join(outDir, "runtime-v03", "opencode.yaml"), "utf8"));
|
||||
const opencodeDeployment = (opencode.items ?? []).find((item) => item.kind === "Deployment" && item.metadata?.name === "opencode-server");
|
||||
assert.ok(opencodeDeployment, "expected opencode-server deployment");
|
||||
assert.equal(opencodeDeployment.metadata?.annotations?.["hwlab.pikastech.local/opencode-provider-profile"], "dsflash-go");
|
||||
assert.equal(opencodeDeployment.metadata?.annotations?.["hwlab.pikastech.local/agentrun-provider-secret-ref"], "agentrun-v02/agentrun-v01-provider-dsflash-go");
|
||||
assert.equal(opencodeDeployment.metadata?.annotations?.["hwlab.pikastech.local/values-printed"], "false");
|
||||
assert.match(opencodeDeployment.spec?.template?.metadata?.annotations?.["hwlab.pikastech.local/opencode-config-sha256"] ?? "", /^[a-f0-9]{64}$/u);
|
||||
const opencodeContainer = collectContainersFromItem(opencodeDeployment).find((container) => container.name === "opencode-server");
|
||||
const opencodeEnvEntries = new Map((opencodeContainer?.env ?? []).map((entry) => [entry.name, entry]));
|
||||
assert.deepEqual(opencodeEnvEntries.get("OPENCODE_DSFLASH_GO_API_KEY")?.valueFrom?.secretKeyRef, { name: "hwlab-v03-code-agent-provider", key: "opencode-api-key", optional: true });
|
||||
const opencodeConfig = JSON.parse(opencodeEnvEntries.get("OPENCODE_CONFIG_CONTENT")?.value ?? "{}");
|
||||
assert.equal(opencodeConfig.model, "dsflash-go/deepseek-v4-flash");
|
||||
assert.equal(opencodeConfig.small_model, "dsflash-go/deepseek-v4-flash");
|
||||
assert.equal(opencodeConfig.provider["dsflash-go"].npm, "@ai-sdk/openai-compatible");
|
||||
assert.equal(opencodeConfig.provider["dsflash-go"].options.baseURL, "https://opencode.ai/zen/go/v1");
|
||||
assert.equal(opencodeConfig.provider["dsflash-go"].options.apiKey, "{env:OPENCODE_DSFLASH_GO_API_KEY}");
|
||||
assert.deepEqual(opencodeConfig.provider["dsflash-go"].models["deepseek-v4-flash"].limit, { context: 1000000, output: 384000 });
|
||||
} finally {
|
||||
await rm(outDir, { recursive: true, force: true });
|
||||
await rm(scratchDir, { recursive: true, force: true });
|
||||
|
||||
Reference in New Issue
Block a user