Skip to content

Commit 68834cf

Browse files
authored
fix(opencode): normalize provider metadata and tag otel runs (#23140)
1 parent 5621373 commit 68834cf

9 files changed

Lines changed: 112 additions & 37 deletions

File tree

packages/opencode/src/cli/cmd/tui/thread.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { EventSource } from "./context/sdk"
1515
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
1616
import { writeHeapSnapshot } from "v8"
1717
import { TuiConfig } from "./config/tui"
18+
import { OPENCODE_PROCESS_ROLE, OPENCODE_RUN_ID, ensureRunID, sanitizedProcessEnv } from "@/util/opencode-process"
1819

1920
declare global {
2021
const OPENCODE_WORKER_PATH: string
@@ -129,11 +130,13 @@ export const TuiThreadCommand = cmd({
129130
return
130131
}
131132
const cwd = Filesystem.resolve(process.cwd())
133+
const env = sanitizedProcessEnv({
134+
[OPENCODE_PROCESS_ROLE]: "worker",
135+
[OPENCODE_RUN_ID]: ensureRunID(),
136+
})
132137

133138
const worker = new Worker(file, {
134-
env: Object.fromEntries(
135-
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
136-
),
139+
env,
137140
})
138141
worker.onerror = (e) => {
139142
Log.Default.error("thread error", {

packages/opencode/src/cli/cmd/tui/worker.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import { Flag } from "@/flag/flag"
1111
import { writeHeapSnapshot } from "node:v8"
1212
import { Heap } from "@/cli/heap"
1313
import { AppRuntime } from "@/effect/app-runtime"
14+
import { ensureProcessMetadata } from "@/util/opencode-process"
15+
16+
ensureProcessMetadata("worker")
1417

1518
await Log.init({
1619
print: process.argv.includes("--print-logs"),

packages/opencode/src/effect/observability.ts

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { OtlpLogger, OtlpSerialization } from "effect/unstable/observability"
44
import * as EffectLogger from "./logger"
55
import { Flag } from "@/flag/flag"
66
import { InstallationChannel, InstallationVersion } from "@/installation/version"
7+
import { ensureProcessMetadata } from "@/util/opencode-process"
78

89
const base = Flag.OTEL_EXPORTER_OTLP_ENDPOINT
910
export const enabled = !!base
11+
const processID = crypto.randomUUID()
1012

1113
const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS
1214
? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce(
@@ -19,26 +21,34 @@ const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS
1921
)
2022
: undefined
2123

22-
const resource = {
23-
serviceName: "opencode",
24-
serviceVersion: InstallationVersion,
25-
attributes: {
26-
"deployment.environment.name": InstallationChannel,
27-
"opencode.client": Flag.OPENCODE_CLIENT,
28-
},
24+
function resource() {
25+
const processMetadata = ensureProcessMetadata("main")
26+
return {
27+
serviceName: "opencode",
28+
serviceVersion: InstallationVersion,
29+
attributes: {
30+
"deployment.environment.name": InstallationChannel,
31+
"opencode.client": Flag.OPENCODE_CLIENT,
32+
"opencode.process_role": processMetadata.processRole,
33+
"opencode.run_id": processMetadata.runID,
34+
"service.instance.id": processID,
35+
},
36+
}
2937
}
3038

31-
const logs = Logger.layer(
32-
[
33-
EffectLogger.logger,
34-
OtlpLogger.make({
35-
url: `${base}/v1/logs`,
36-
resource,
37-
headers,
38-
}),
39-
],
40-
{ mergeWithExisting: false },
41-
).pipe(Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer))
39+
function logs() {
40+
return Logger.layer(
41+
[
42+
EffectLogger.logger,
43+
OtlpLogger.make({
44+
url: `${base}/v1/logs`,
45+
resource: resource(),
46+
headers,
47+
}),
48+
],
49+
{ mergeWithExisting: false },
50+
).pipe(Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer))
51+
}
4252

4353
const traces = async () => {
4454
const NodeSdk = await import("@effect/opentelemetry/NodeSdk")
@@ -58,7 +68,7 @@ const traces = async () => {
5868
context.setGlobalContextManager(mgr)
5969

6070
return NodeSdk.layer(() => ({
61-
resource,
71+
resource: resource(),
6272
spanProcessor: new SdkBase.BatchSpanProcessor(
6373
new OTLP.OTLPTraceExporter({
6474
url: `${base}/v1/traces`,
@@ -73,7 +83,7 @@ export const layer = !base
7383
: Layer.unwrap(
7484
Effect.gen(function* () {
7585
const trace = yield* Effect.promise(traces)
76-
return Layer.mergeAll(trace, logs)
86+
return Layer.mergeAll(trace, logs())
7787
}),
7888
)
7989

packages/opencode/src/file/ripgrep.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ripgrep } from "ripgrep"
77

88
import { Filesystem } from "@/util"
99
import { Log } from "@/util"
10+
import { sanitizedProcessEnv } from "@/util/opencode-process"
1011

1112
const log = Log.create({ service: "ripgrep" })
1213

@@ -157,9 +158,7 @@ type WorkerError = {
157158
}
158159

159160
function env() {
160-
const env = Object.fromEntries(
161-
Object.entries(process.env).filter((item): item is [string, string] => item[1] !== undefined),
162-
)
161+
const env = sanitizedProcessEnv()
163162
delete env.RIPGREP_CONFIG_PATH
164163
return env
165164
}

packages/opencode/src/file/ripgrep.worker.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { ripgrep } from "ripgrep"
2+
import { sanitizedProcessEnv } from "@/util/opencode-process"
23

34
function env() {
4-
const env = Object.fromEntries(
5-
Object.entries(process.env).filter((item): item is [string, string] => item[1] !== undefined),
6-
)
5+
const env = sanitizedProcessEnv()
76
delete env.RIPGREP_CONFIG_PATH
87
return env
98
}

packages/opencode/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ import { errorMessage } from "./util/error"
3838
import { PluginCommand } from "./cli/cmd/plug"
3939
import { Heap } from "./cli/heap"
4040
import { drizzle } from "drizzle-orm/bun-sqlite"
41+
import { ensureProcessMetadata } from "./util/opencode-process"
42+
43+
const processMetadata = ensureProcessMetadata("main")
4144

4245
process.on("unhandledRejection", (e) => {
4346
Log.Default.error("rejection", {
@@ -108,6 +111,8 @@ const cli = yargs(args)
108111
Log.Default.info("opencode", {
109112
version: InstallationVersion,
110113
args: process.argv.slice(2),
114+
process_role: processMetadata.processRole,
115+
run_id: processMetadata.runID,
111116
})
112117

113118
const marker = path.join(Global.Path.data, "opencode.db")

packages/opencode/src/provider/provider.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -968,7 +968,7 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model
968968
family: model.family,
969969
api: {
970970
id: model.id,
971-
url: model.provider?.api ?? provider.api!,
971+
url: model.provider?.api ?? provider.api ?? "",
972972
npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible",
973973
},
974974
status: model.status ?? "active",
@@ -981,10 +981,10 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model
981981
output: model.limit.output,
982982
},
983983
capabilities: {
984-
temperature: model.temperature,
985-
reasoning: model.reasoning,
986-
attachment: model.attachment,
987-
toolcall: model.tool_call,
984+
temperature: model.temperature ?? false,
985+
reasoning: model.reasoning ?? false,
986+
attachment: model.attachment ?? false,
987+
toolcall: model.tool_call ?? true,
988988
input: {
989989
text: model.modalities?.input?.includes("text") ?? false,
990990
audio: model.modalities?.input?.includes("audio") ?? false,
@@ -1001,7 +1001,7 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model
10011001
},
10021002
interleaved: model.interleaved ?? false,
10031003
},
1004-
release_date: model.release_date,
1004+
release_date: model.release_date ?? "",
10051005
variants: {},
10061006
}
10071007

@@ -1143,7 +1143,7 @@ const layer: Layer.Layer<
11431143
existingModel?.api.npm ??
11441144
modelsDev[providerID]?.npm ??
11451145
"@ai-sdk/openai-compatible",
1146-
url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api,
1146+
url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api ?? "",
11471147
},
11481148
status: model.status ?? existingModel?.status ?? "active",
11491149
name,
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export const OPENCODE_RUN_ID = "OPENCODE_RUN_ID"
2+
export const OPENCODE_PROCESS_ROLE = "OPENCODE_PROCESS_ROLE"
3+
4+
export function ensureRunID() {
5+
return (process.env[OPENCODE_RUN_ID] ??= crypto.randomUUID())
6+
}
7+
8+
export function ensureProcessRole(fallback: "main" | "worker") {
9+
return (process.env[OPENCODE_PROCESS_ROLE] ??= fallback)
10+
}
11+
12+
export function ensureProcessMetadata(fallback: "main" | "worker") {
13+
return {
14+
runID: ensureRunID(),
15+
processRole: ensureProcessRole(fallback),
16+
}
17+
}
18+
19+
export function sanitizedProcessEnv(overrides?: Record<string, string>) {
20+
const env = Object.fromEntries(
21+
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
22+
)
23+
return overrides ? Object.assign(env, overrides) : env
24+
}

packages/opencode/test/provider/provider.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1916,7 +1916,7 @@ test("mode cost preserves over-200k pricing from base model", () => {
19161916
},
19171917
},
19181918
},
1919-
} as ModelsDev.Provider
1919+
} as unknown as ModelsDev.Provider
19201920

19211921
const model = Provider.fromModelsDevProvider(provider).models["gpt-5.4-fast"]
19221922
expect(model.cost.input).toEqual(5)
@@ -1934,6 +1934,38 @@ test("mode cost preserves over-200k pricing from base model", () => {
19341934
})
19351935
})
19361936

1937+
test("models.dev normalization fills required response fields", () => {
1938+
const provider = {
1939+
id: "gateway",
1940+
name: "Gateway",
1941+
env: [],
1942+
models: {
1943+
"gpt-5.4": {
1944+
id: "gpt-5.4",
1945+
name: "GPT-5.4",
1946+
family: "gpt",
1947+
cost: {
1948+
input: 2.5,
1949+
output: 15,
1950+
},
1951+
limit: {
1952+
context: 1_050_000,
1953+
input: 922_000,
1954+
output: 128_000,
1955+
},
1956+
},
1957+
},
1958+
} as unknown as ModelsDev.Provider
1959+
1960+
const model = Provider.fromModelsDevProvider(provider).models["gpt-5.4"]
1961+
expect(model.api.url).toBe("")
1962+
expect(model.capabilities.temperature).toBe(false)
1963+
expect(model.capabilities.reasoning).toBe(false)
1964+
expect(model.capabilities.attachment).toBe(false)
1965+
expect(model.capabilities.toolcall).toBe(true)
1966+
expect(model.release_date).toBe("")
1967+
})
1968+
19371969
test("model variants are generated for reasoning models", async () => {
19381970
await using tmp = await tmpdir({
19391971
init: async (dir) => {

0 commit comments

Comments
 (0)