Compare commits

...

3 Commits

Author SHA1 Message Date
Dax Raad dcc924386d refactor(core): decouple session version 2026-07-21 14:07:25 -04:00
Dax Raad 689e17738c Merge remote-tracking branch 'origin/v2' into client-service
# Conflicts:
#	packages/core/src/session/generate-node.ts
#	packages/core/src/session/title.ts
#	packages/util/src/observability.ts
2026-07-21 13:10:07 -04:00
Dax Raad e2f203a56e refactor: centralize client identity 2026-07-21 12:58:16 -04:00
14 changed files with 103 additions and 96 deletions
+3 -3
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import { NodeRuntime, NodeServices } from "@effect/platform-node" import { NodeRuntime, NodeServices } from "@effect/platform-node"
import { Effect } from "effect" import { Effect, Layer } from "effect"
import { Commands } from "./commands/commands" import { Commands } from "./commands/commands"
import { Runtime } from "./framework/runtime" import { Runtime } from "./framework/runtime"
import { Observability } from "@opencode-ai/util/observability" import { Observability } from "@opencode-ai/util/observability"
import { Client } from "@opencode-ai/util/client"
import { Updater } from "./services/updater" import { Updater } from "./services/updater"
import { InstallationChannel, InstallationVersion, InstallationLocal } from "@opencode-ai/util/installation/version" import { InstallationChannel, InstallationVersion, InstallationLocal } from "@opencode-ai/util/installation/version"
import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { LayerNode } from "@opencode-ai/util/effect/layer-node"
@@ -73,8 +74,7 @@ Effect.logInfo("cli starting", {
Observability.layer({ Observability.layer({
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
headers: process.env.OTEL_EXPORTER_OTLP_HEADERS, headers: process.env.OTEL_EXPORTER_OTLP_HEADERS,
client: process.env.OPENCODE_CLIENT ?? "cli", }).pipe(Layer.provide(Client.layer(process.env.OPENCODE_CLIENT))),
}),
), ),
Effect.provide(NodeServices.layer), Effect.provide(NodeServices.layer),
Effect.scoped, Effect.scoped,
+11 -9
View File
@@ -3,6 +3,7 @@ import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effe
import { HttpClient, HttpClientRequest } from "effect/unstable/http" import { HttpClient, HttpClientRequest } from "effect/unstable/http"
import { ModelsDev } from "@opencode-ai/schema/models-dev" import { ModelsDev } from "@opencode-ai/schema/models-dev"
import { Money } from "@opencode-ai/schema/money" import { Money } from "@opencode-ai/schema/money"
import { Client } from "@opencode-ai/util/client"
import { Global } from "@opencode-ai/util/global" import { Global } from "@opencode-ai/util/global"
import { Flock } from "@opencode-ai/util/flock" import { Flock } from "@opencode-ai/util/flock"
import { Hash } from "@opencode-ai/util/hash" import { Hash } from "@opencode-ai/util/hash"
@@ -531,17 +532,18 @@ export const Options = Schema.Struct({
url: Schema.optional(Schema.String), url: Schema.optional(Schema.String),
file: Schema.optional(Schema.String), file: Schema.optional(Schema.String),
fetch: Schema.optional(Schema.Boolean), fetch: Schema.optional(Schema.Boolean),
client: Schema.optional(Schema.String),
}) })
export type Options = typeof Options.Type export type Options = typeof Options.Type
export class Service extends Context.Service<Service, Interface>()("@opencode/ModelsDev") {} export class Service extends Context.Service<Service, Interface>()("@opencode/ModelsDev") {}
export const layer = (options?: Options) => Layer.effect( export const layer = (options?: Options) =>
Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const fs = yield* FSUtil.Service const fs = yield* FSUtil.Service
const events = yield* EventV2.Service const events = yield* EventV2.Service
const client = yield* Client.Name
const http = HttpClient.filterStatusOk( const http = HttpClient.filterStatusOk(
(yield* HttpClient.HttpClient).pipe( (yield* HttpClient.HttpClient).pipe(
HttpClient.retryTransient({ HttpClient.retryTransient({
@@ -554,7 +556,7 @@ export const layer = (options?: Options) => Layer.effect(
const source = options?.url || "https://models.dev" const source = options?.url || "https://models.dev"
const fetch = options?.fetch ?? true const fetch = options?.fetch ?? true
const userAgent = `opencode/${InstallationChannel}/${InstallationVersion}/${options?.client ?? "cli"}` const userAgent = `opencode/${InstallationChannel}/${InstallationVersion}/${client}`
const filepath = path.join( const filepath = path.join(
Global.Path.cache, Global.Path.cache,
source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`, source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`,
@@ -581,11 +583,7 @@ export const layer = (options?: Options) => Layer.effect(
const loadFromDisk = fs.readJson(options?.file ?? filepath).pipe( const loadFromDisk = fs.readJson(options?.file ?? filepath).pipe(
Effect.map((input) => input as Record<string, SourceProvider>), Effect.map((input) => input as Record<string, SourceProvider>),
Effect.catch((error) => { Effect.catch((error) => {
if ( if (options?.file === undefined && error._tag === "FileSystemError" && error.method === "readJson") {
options?.file === undefined &&
error._tag === "FileSystemError" &&
error.method === "readJson"
) {
return fs.remove(filepath, { force: true }).pipe(Effect.ignore, Effect.as(undefined)) return fs.remove(filepath, { force: true }).pipe(Effect.ignore, Effect.as(undefined))
} }
return Effect.succeed(undefined) return Effect.succeed(undefined)
@@ -659,7 +657,11 @@ export const layer = (options?: Options) => Layer.effect(
) )
export function configured(options?: Options) { export function configured(options?: Options) {
return makeGlobalNode({ service: Service, layer: layer(options), deps: [FSUtil.node, EventV2.node, httpClient] }) return makeGlobalNode({
service: Service,
layer: layer(options),
deps: [FSUtil.node, EventV2.node, Client.node, httpClient],
})
} }
export const node = configured() export const node = configured()
+1 -2
View File
@@ -20,7 +20,6 @@ import { AbsolutePath, PositiveInt, RelativePath } from "./schema"
import { AgentV2 } from "./agent" import { AgentV2 } from "./agent"
import { SessionV1 } from "./v1/session" import { SessionV1 } from "./v1/session"
import { Money } from "@opencode-ai/schema/money" import { Money } from "@opencode-ai/schema/money"
import { InstallationVersion } from "@opencode-ai/util/installation/version"
import { Slug } from "./util/slug" import { Slug } from "./util/slug"
import { ProjectTable } from "./project/sql" import { ProjectTable } from "./project/sql"
import path from "path" import path from "path"
@@ -352,7 +351,7 @@ const layer = Layer.effect(
const info = SessionV1.SessionInfo.make({ const info = SessionV1.SessionInfo.make({
id: sessionID, id: sessionID,
slug: Slug.create(), slug: Slug.create(),
version: InstallationVersion, version: "unknown",
projectID: project.id, projectID: project.id,
parentID: input.parentID, parentID: input.parentID,
directory: location.directory, directory: location.directory,
+11 -13
View File
@@ -10,6 +10,7 @@ import { llmClient } from "../effect/app-node-platform"
import { SessionEvent } from "./event" import { SessionEvent } from "./event"
import type { SessionMessage } from "./message" import type { SessionMessage } from "./message"
import { SessionModelHeaders } from "./model-headers" import { SessionModelHeaders } from "./model-headers"
import { Client } from "@opencode-ai/util/client"
import { SessionRunnerModel } from "./runner/model" import { SessionRunnerModel } from "./runner/model"
import { SessionSchema } from "./schema" import { SessionSchema } from "./schema"
import { toSessionError } from "./to-session-error" import { toSessionError } from "./to-session-error"
@@ -60,7 +61,7 @@ type Settings = {
} }
type Dependencies = { type Dependencies = {
readonly headers?: SessionModelHeaders.Options readonly client: string
readonly events: EventV2.Interface readonly events: EventV2.Interface
readonly llm: { readonly llm: {
readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, LLMError> readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, LLMError>
@@ -259,7 +260,7 @@ const make = (dependencies: Dependencies) => {
.stream( .stream(
LLM.request({ LLM.request({
model: plan.model, model: plan.model,
http: { headers: SessionModelHeaders.make(plan.session, dependencies.headers) }, http: { headers: SessionModelHeaders.make(plan.session, dependencies.client) },
messages: [Message.user(plan.prompt)], messages: [Message.user(plan.prompt)],
tools: [], tools: [],
}), }),
@@ -391,23 +392,20 @@ const make = (dependencies: Dependencies) => {
}) })
} }
export const layer = (options?: SessionModelHeaders.Options) => Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const llm = yield* LLMClient.Service const llm = yield* LLMClient.Service
const config = yield* Config.Service const config = yield* Config.Service
const models = yield* SessionRunnerModel.Service const models = yield* SessionRunnerModel.Service
return make({ events, llm, models, config: settings(yield* config.entries()), headers: options }) const client = yield* Client.Name
return make({ events, llm, models, config: settings(yield* config.entries()), client })
}), }),
) )
export function configured(options?: SessionModelHeaders.Options) { export const node = makeLocationNode({
return makeLocationNode({ service: Service,
service: Service, layer,
layer: layer(options), deps: [EventV2.node, llmClient, Config.node, SessionRunnerModel.node, Client.node],
deps: [EventV2.node, llmClient, Config.node, SessionRunnerModel.node], })
})
}
export const node = configured()
+9 -11
View File
@@ -4,6 +4,7 @@ import { LLM, LLMClient, Message, SystemPart } from "@opencode-ai/ai"
import { Effect, Layer } from "effect" import { Effect, Layer } from "effect"
import { Database } from "../database/database" import { Database } from "../database/database"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Client } from "@opencode-ai/util/client"
import { llmClient } from "../effect/app-node-platform" import { llmClient } from "../effect/app-node-platform"
import { PluginHooks } from "../plugin/hooks" import { PluginHooks } from "../plugin/hooks"
import { SessionContext } from "./context" import { SessionContext } from "./context"
@@ -14,7 +15,7 @@ import { SessionRunnerModel } from "./runner/model"
import PROMPT_DEFAULT from "./runner/prompt/base.txt" import PROMPT_DEFAULT from "./runner/prompt/base.txt"
import { toLLMMessages } from "./runner/to-llm-message" import { toLLMMessages } from "./runner/to-llm-message"
export const layer = (options?: SessionModelHeaders.Options) => Layer.effect( export const layer = Layer.effect(
SessionGenerate.Service, SessionGenerate.Service,
Effect.gen(function* () { Effect.gen(function* () {
const context = yield* SessionContext.Service const context = yield* SessionContext.Service
@@ -22,6 +23,7 @@ export const layer = (options?: SessionModelHeaders.Options) => Layer.effect(
const hooks = yield* PluginHooks.Service const hooks = yield* PluginHooks.Service
const llm = yield* LLMClient.Service const llm = yield* LLMClient.Service
const models = yield* SessionRunnerModel.Service const models = yield* SessionRunnerModel.Service
const client = yield* Client.Name
return SessionGenerate.Service.of({ return SessionGenerate.Service.of({
generate: Effect.fn("SessionGenerate.generate")(function* (input) { generate: Effect.fn("SessionGenerate.generate")(function* (input) {
@@ -49,7 +51,7 @@ export const layer = (options?: SessionModelHeaders.Options) => Layer.effect(
return (yield* llm.generate( return (yield* llm.generate(
LLM.request({ LLM.request({
model: model.model, model: model.model,
http: { headers: SessionModelHeaders.make(selection.session, options) }, http: { headers: SessionModelHeaders.make(selection.session, client) },
providerOptions: { openai: { promptCacheKey } }, providerOptions: { openai: { promptCacheKey } },
system: contextEvent.system, system: contextEvent.system,
messages: contextEvent.messages, messages: contextEvent.messages,
@@ -62,12 +64,8 @@ export const layer = (options?: SessionModelHeaders.Options) => Layer.effect(
}), }),
) )
export function configured(options?: SessionModelHeaders.Options) { export const node = makeLocationNode({
return makeLocationNode({ service: SessionGenerate.Service,
service: SessionGenerate.Service, layer,
layer: layer(options), deps: [SessionContext.node, Database.node, PluginHooks.node, SessionRunnerModel.node, Client.node, llmClient],
deps: [SessionContext.node, Database.node, PluginHooks.node, SessionRunnerModel.node, llmClient], })
})
}
export const node = configured()
+2 -11
View File
@@ -2,22 +2,13 @@ export * as SessionModelHeaders from "./model-headers"
import { InstallationVersion } from "@opencode-ai/util/installation/version" import { InstallationVersion } from "@opencode-ai/util/installation/version"
import { SessionSchema } from "./schema" import { SessionSchema } from "./schema"
import { Schema } from "effect"
export const Options = Schema.Struct({ export const make = (session: Pick<SessionSchema.Info, "id" | "parentID" | "projectID">, client: string) => ({
client: Schema.optional(Schema.String),
})
export type Options = typeof Options.Type
export const make = (
session: Pick<SessionSchema.Info, "id" | "parentID" | "projectID">,
options?: Options,
) => ({
"x-session-affinity": session.id, "x-session-affinity": session.id,
"X-Session-Id": session.id, "X-Session-Id": session.id,
...(session.parentID ? { "x-parent-session-id": session.parentID } : {}), ...(session.parentID ? { "x-parent-session-id": session.parentID } : {}),
"User-Agent": `opencode/${InstallationVersion}`, "User-Agent": `opencode/${InstallationVersion}`,
"x-opencode-project": session.projectID, "x-opencode-project": session.projectID,
"x-opencode-session": session.id, "x-opencode-session": session.id,
"x-opencode-client": options?.client ?? "cli", "x-opencode-client": client,
}) })
+9 -7
View File
@@ -4,6 +4,7 @@ import { LLM, Message, SystemPart, type LLMRequest } from "@opencode-ai/ai"
import { SessionError } from "@opencode-ai/schema/session-error" import { SessionError } from "@opencode-ai/schema/session-error"
import { Context, Effect, Layer } from "effect" import { Context, Effect, Layer } from "effect"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Client } from "@opencode-ai/util/client"
import { PluginHooks } from "../plugin/hooks" import { PluginHooks } from "../plugin/hooks"
import { ToolRegistry } from "../tool/registry" import { ToolRegistry } from "../tool/registry"
import { SessionContext } from "./context" import { SessionContext } from "./context"
@@ -39,11 +40,12 @@ export interface Interface {
/** Location-scoped outbound model-request preparation. */ /** Location-scoped outbound model-request preparation. */
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionModelRequest") {} export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionModelRequest") {}
export const layer = (options?: SessionModelHeaders.Options) => Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const hooks = yield* PluginHooks.Service const hooks = yield* PluginHooks.Service
const registry = yield* ToolRegistry.Service const registry = yield* ToolRegistry.Service
const client = yield* Client.Name
const prepare = Effect.fn("SessionModelRequest.prepare")(function* (input: PrepareInput) { const prepare = Effect.fn("SessionModelRequest.prepare")(function* (input: PrepareInput) {
const session = input.context.session const session = input.context.session
@@ -81,7 +83,7 @@ export const layer = (options?: SessionModelHeaders.Options) => Layer.effect(
const request = LLM.request({ const request = LLM.request({
model, model,
http: { http: {
headers: SessionModelHeaders.make(session, options), headers: SessionModelHeaders.make(session, client),
}, },
providerOptions: { openai: { promptCacheKey } }, providerOptions: { openai: { promptCacheKey } },
system: contextEvent.system, system: contextEvent.system,
@@ -112,8 +114,8 @@ export const layer = (options?: SessionModelHeaders.Options) => Layer.effect(
}), }),
) )
export function configured(options?: SessionModelHeaders.Options) { export const node = makeLocationNode({
return makeLocationNode({ service: Service, layer: layer(options), deps: [PluginHooks.node, ToolRegistry.node] }) service: Service,
} layer,
deps: [PluginHooks.node, ToolRegistry.node, Client.node],
export const node = configured() })
+11 -13
View File
@@ -6,6 +6,7 @@ import { AgentV2 } from "../agent"
import { Database } from "../database/database" import { Database } from "../database/database"
import { EventV2 } from "../event" import { EventV2 } from "../event"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Client } from "@opencode-ai/util/client"
import { llmClient } from "../effect/app-node-platform" import { llmClient } from "../effect/app-node-platform"
import { SessionEvent } from "./event" import { SessionEvent } from "./event"
import { SessionHistory } from "./history" import { SessionHistory } from "./history"
@@ -17,7 +18,7 @@ import { SessionUsage } from "./usage"
const MAX_LENGTH = 100 const MAX_LENGTH = 100
type Dependencies = { type Dependencies = {
readonly headers?: SessionModelHeaders.Options readonly client: string
readonly events: EventV2.Interface readonly events: EventV2.Interface
readonly llm: { readonly llm: {
readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, LLMError> readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, LLMError>
@@ -67,7 +68,7 @@ const make = (dependencies: Dependencies) => {
.stream( .stream(
LLM.request({ LLM.request({
model: resolved.model, model: resolved.model,
http: { headers: SessionModelHeaders.make(session, dependencies.headers) }, http: { headers: SessionModelHeaders.make(session, dependencies.client) },
system: agent.system, system: agent.system,
messages: [Message.user(firstUser.text)], messages: [Message.user(firstUser.text)],
tools: [], tools: [],
@@ -103,7 +104,7 @@ const make = (dependencies: Dependencies) => {
return { generateForFirstPrompt } return { generateForFirstPrompt }
} }
export const layer = (options?: SessionModelHeaders.Options) => Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
@@ -111,19 +112,16 @@ export const layer = (options?: SessionModelHeaders.Options) => Layer.effect(
const agents = yield* AgentV2.Service const agents = yield* AgentV2.Service
const models = yield* SessionRunnerModel.Service const models = yield* SessionRunnerModel.Service
const database = yield* Database.Service const database = yield* Database.Service
const title = make({ events, llm, agents, models, headers: options }) const client = yield* Client.Name
const title = make({ events, llm, agents, models, client })
return Service.of({ return Service.of({
generateForFirstPrompt: (session) => title.generateForFirstPrompt(database.db, session), generateForFirstPrompt: (session) => title.generateForFirstPrompt(database.db, session),
}) })
}), }),
) )
export function configured(options?: SessionModelHeaders.Options) { export const node = makeLocationNode({
return makeLocationNode({ service: Service,
service: Service, layer,
layer: layer(options), deps: [EventV2.node, llmClient, AgentV2.node, SessionRunnerModel.node, Database.node, Client.node],
deps: [EventV2.node, llmClient, AgentV2.node, SessionRunnerModel.node, Database.node], })
})
}
export const node = configured()
+1 -1
View File
@@ -56,7 +56,7 @@ const locations = Layer.effect(
() => () =>
// The test only needs the compaction location service used by SessionV2.compact. // The test only needs the compaction location service used by SessionV2.compact.
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion
SessionCompaction.layer().pipe( SessionCompaction.layer.pipe(
Layer.provide(client), Layer.provide(client),
Layer.provide(config), Layer.provide(config),
Layer.provide(models), Layer.provide(models),
+5 -3
View File
@@ -9,9 +9,11 @@ export const create = Effect.fn("OpenCode.create")(function* (options: ServerOpt
const runtime = yield* Effect.acquireRelease( const runtime = yield* Effect.acquireRelease(
Effect.sync(() => Effect.sync(() =>
ManagedRuntime.make( ManagedRuntime.make(
createEmbeddedRoutes({ ...options, database: { path: ":memory:", ...options.database } }).pipe( createEmbeddedRoutes({
Layer.provide(HttpServer.layerServices), ...options,
), client: options.client ?? "sdk",
database: { path: ":memory:", ...options.database },
}).pipe(Layer.provide(HttpServer.layerServices)),
), ),
), ),
(runtime) => runtime.disposeEffect, (runtime) => runtime.disposeEffect,
+4 -6
View File
@@ -6,6 +6,7 @@ import { EventV2 } from "@opencode-ai/core/event"
import { EventLogger } from "@opencode-ai/core/event-logger" import { EventLogger } from "@opencode-ai/core/event-logger"
import { FileSystemSearch } from "@opencode-ai/core/filesystem/search" import { FileSystemSearch } from "@opencode-ai/core/filesystem/search"
import { Observability } from "@opencode-ai/util/observability" import { Observability } from "@opencode-ai/util/observability"
import { Client } from "@opencode-ai/util/client"
import { Credential } from "@opencode-ai/core/credential" import { Credential } from "@opencode-ai/core/credential"
import { Config } from "@opencode-ai/core/config" import { Config } from "@opencode-ai/core/config"
import { CommandV2 } from "@opencode-ai/core/command" import { CommandV2 } from "@opencode-ai/core/command"
@@ -87,7 +88,8 @@ function makeRoutes<AuthError, AuthServices>(
const pluginRuntimeCell = PluginRuntime.makeCell() const pluginRuntimeCell = PluginRuntime.makeCell()
const replacements: LayerNode.Replacements = [ const replacements: LayerNode.Replacements = [
[Database.node, Database.configured(options.database)], [Database.node, Database.configured(options.database)],
[ModelsDev.node, ModelsDev.configured({ ...options.models, client: options.client })], [Client.node, Client.configured(options.client)],
[ModelsDev.node, ModelsDev.configured(options.models)],
[Watcher.node, Watcher.configured({ enabled: options.fs?.filewatcher })], [Watcher.node, Watcher.configured({ enabled: options.fs?.filewatcher })],
[FileSystemSearch.node, FileSystemSearch.configured({ fff: options.fs?.fff })], [FileSystemSearch.node, FileSystemSearch.configured({ fff: options.fs?.fff })],
[Global.node, Global.layerWith(options.config?.directory ? { config: options.config.directory } : {})], [Global.node, Global.layerWith(options.config?.directory ? { config: options.config.directory } : {})],
@@ -103,10 +105,6 @@ function makeRoutes<AuthError, AuthServices>(
[CommandV2.node, CommandV2.configured({ gitbash: options.windows?.gitbash })], [CommandV2.node, CommandV2.configured({ gitbash: options.windows?.gitbash })],
[Pty.node, Pty.configured({ gitbash: options.windows?.gitbash })], [Pty.node, Pty.configured({ gitbash: options.windows?.gitbash })],
[Shell.node, Shell.configured({ gitbash: options.windows?.gitbash })], [Shell.node, Shell.configured({ gitbash: options.windows?.gitbash })],
[SessionCompaction.node, SessionCompaction.configured({ client: options.client })],
[SessionGenerateNode.node, SessionGenerateNode.configured({ client: options.client })],
[SessionModelRequest.node, SessionModelRequest.configured({ client: options.client })],
[SessionTitle.node, SessionTitle.configured({ client: options.client })],
[PluginRuntime.node, PluginRuntime.layerWithCell(pluginRuntimeCell)], [PluginRuntime.node, PluginRuntime.layerWithCell(pluginRuntimeCell)],
[PluginRuntime.providerNode, PluginRuntime.providerNodeWithCell(pluginRuntimeCell)], [PluginRuntime.providerNode, PluginRuntime.providerNodeWithCell(pluginRuntimeCell)],
] ]
@@ -135,7 +133,7 @@ function makeRoutes<AuthError, AuthServices>(
Layer.provide(authorizationLayer), Layer.provide(authorizationLayer),
Layer.provide(schemaErrorLayer), Layer.provide(schemaErrorLayer),
Layer.provide(auth), Layer.provide(auth),
Layer.provide(Observability.layer({ ...options.observability, client: options.client })), Layer.provide(Observability.layer(options.observability).pipe(Layer.provide(Client.layer(options.client)))),
HttpRouter.provideRequest(requestServices), HttpRouter.provideRequest(requestServices),
Layer.provideMerge(services), Layer.provideMerge(services),
Layer.provideMerge(HttpRouter.layer), Layer.provideMerge(HttpRouter.layer),
+14
View File
@@ -0,0 +1,14 @@
export * as Client from "./client.js"
import { Context, Layer } from "effect"
import { makeGlobalNode } from "./effect/app-node.js"
export const Name = Context.Reference<string>("@opencode/Client/Name", {
defaultValue: () => "cli",
})
export const layer = (name = "cli") => Layer.succeed(Name, name)
export const configured = (name?: string) => makeGlobalNode({ service: Name, layer: layer(name), deps: [] })
export const node = configured()
+6 -4
View File
@@ -7,11 +7,11 @@ import { FetchHttpClient } from "effect/unstable/http"
import { OtlpSerialization } from "effect/unstable/observability" import { OtlpSerialization } from "effect/unstable/observability"
import { Logging } from "./observability/logging.js" import { Logging } from "./observability/logging.js"
import { Otlp } from "./observability/otlp.js" import { Otlp } from "./observability/otlp.js"
import { Client } from "./client.js"
export const Options = Schema.Struct({ export const Options = Schema.Struct({
endpoint: Schema.optional(Schema.String), endpoint: Schema.optional(Schema.String),
headers: Schema.optional(Schema.String), headers: Schema.optional(Schema.String),
client: Schema.optional(Schema.String),
}) })
export type Options = typeof Options.Type export type Options = typeof Options.Type
@@ -19,7 +19,6 @@ export function layer(
options: Options = { options: Options = {
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
headers: process.env.OTEL_EXPORTER_OTLP_HEADERS, headers: process.env.OTEL_EXPORTER_OTLP_HEADERS,
client: process.env.OPENCODE_CLIENT ?? "cli",
}, },
) { ) {
const local = Logger.layer(Logging.loggers(), { mergeWithExisting: false }).pipe( const local = Logger.layer(Logging.loggers(), { mergeWithExisting: false }).pipe(
@@ -29,14 +28,17 @@ export function layer(
) )
return Layer.unwrap( return Layer.unwrap(
Effect.gen(function* () { Effect.gen(function* () {
const logs = Logger.layer([...Logging.loggers(), ...Otlp.loggers(options)], { mergeWithExisting: false }).pipe( const client = yield* Client.Name
const logs = Logger.layer([...Logging.loggers(), ...Otlp.loggers(options, client)], {
mergeWithExisting: false,
}).pipe(
Layer.provide(NodeFileSystem.layer), Layer.provide(NodeFileSystem.layer),
Layer.provide(OtlpSerialization.layerJson), Layer.provide(OtlpSerialization.layerJson),
Layer.provide(FetchHttpClient.layer), Layer.provide(FetchHttpClient.layer),
Layer.orDie, Layer.orDie,
Layer.merge(Layer.succeed(References.MinimumLogLevel, Logging.minimumLogLevel())), Layer.merge(Layer.succeed(References.MinimumLogLevel, Logging.minimumLogLevel())),
) )
return Layer.merge(logs, yield* Effect.promise(() => Otlp.tracingLayer(options))) return Layer.merge(logs, yield* Effect.promise(() => Otlp.tracingLayer(options, client)))
}), }),
).pipe(Layer.catchCause(() => local)) ).pipe(Layer.catchCause(() => local))
} }
+16 -13
View File
@@ -6,19 +6,18 @@ import { runID } from "./shared.js"
export interface Options { export interface Options {
readonly endpoint?: string readonly endpoint?: string
readonly headers?: string readonly headers?: string
readonly client?: string
} }
function parseHeaders(value?: string) { function parseHeaders(value?: string) {
return value return value
? value.split(",").reduce( ? value.split(",").reduce(
(acc, entry) => { (acc, entry) => {
const [key, ...value] = entry.split("=") const [key, ...value] = entry.split("=")
acc[key] = value.join("=") acc[key] = value.join("=")
return acc return acc
}, },
{} as Record<string, string>, {} as Record<string, string>,
) )
: undefined : undefined
} }
@@ -38,7 +37,11 @@ function resourceAttributes() {
} }
} }
export function resource(client = "cli"): { serviceName: string; serviceVersion: string; attributes: Record<string, string> } { export function resource(client = "cli"): {
serviceName: string
serviceVersion: string
attributes: Record<string, string>
} {
return { return {
serviceName: "opencode", serviceName: "opencode",
serviceVersion: InstallationVersion, serviceVersion: InstallationVersion,
@@ -52,18 +55,18 @@ export function resource(client = "cli"): { serviceName: string; serviceVersion:
} }
} }
export function loggers(options?: Options) { export function loggers(options: Options | undefined, client: string) {
if (!options?.endpoint) return [] if (!options?.endpoint) return []
return [ return [
OtlpLogger.make({ OtlpLogger.make({
url: `${options.endpoint}/v1/logs`, url: `${options.endpoint}/v1/logs`,
resource: resource(options.client), resource: resource(client),
headers: parseHeaders(options.headers), headers: parseHeaders(options.headers),
}), }),
] ]
} }
export async function tracingLayer(options?: Options) { export async function tracingLayer(options: Options | undefined, client: string) {
if (!options?.endpoint) return Layer.empty if (!options?.endpoint) return Layer.empty
const NodeSdk = await import("@effect/opentelemetry/NodeSdk") const NodeSdk = await import("@effect/opentelemetry/NodeSdk")
const OTLP = await import("@opentelemetry/exporter-trace-otlp-http") const OTLP = await import("@opentelemetry/exporter-trace-otlp-http")
@@ -77,7 +80,7 @@ export async function tracingLayer(options?: Options) {
context.setGlobalContextManager(manager) context.setGlobalContextManager(manager)
return NodeSdk.layer(() => ({ return NodeSdk.layer(() => ({
resource: resource(options.client), resource: resource(client),
spanProcessor: new SdkBase.BatchSpanProcessor( spanProcessor: new SdkBase.BatchSpanProcessor(
new OTLP.OTLPTraceExporter({ new OTLP.OTLPTraceExporter({
url: `${options.endpoint}/v1/traces`, url: `${options.endpoint}/v1/traces`,