feat(plugin): expose app metadata (#38179)
This commit is contained in:
parent
2ed8fe5960
commit
8de40be6ea
71 changed files with 477 additions and 286 deletions
33
packages/core/src/app.ts
Normal file
33
packages/core/src/app.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
export * as App from "./app"
|
||||
|
||||
import { Context, Layer } from "effect"
|
||||
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
|
||||
|
||||
export interface Info {
|
||||
readonly name: string
|
||||
readonly version: string
|
||||
readonly channel: string
|
||||
}
|
||||
|
||||
export const Metadata = Context.Reference<Info>("@opencode/App", {
|
||||
defaultValue: () => make(),
|
||||
})
|
||||
|
||||
export function make(input: Partial<Info> = {}): Info {
|
||||
return {
|
||||
name: input.name ?? "opencode",
|
||||
version: input.version ?? "unknown",
|
||||
channel: input.channel ?? "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
export function useragent(app: Info) {
|
||||
return `opencode/${app.channel}/${app.version}/${app.name}`
|
||||
}
|
||||
|
||||
export const layer = (input?: Partial<Info>) => Layer.succeed(Metadata, make(input))
|
||||
|
||||
export const configured = (input?: Partial<Info>) =>
|
||||
makeGlobalNode({ service: Metadata, layer: layer(input), deps: [] })
|
||||
|
||||
export const node = configured()
|
||||
|
|
@ -12,6 +12,7 @@ import {
|
|||
ElicitationCompleteNotificationSchema,
|
||||
ElicitRequestSchema,
|
||||
GetPromptResultSchema,
|
||||
type Implementation,
|
||||
type ElicitRequestFormParams,
|
||||
type ElicitRequestParams,
|
||||
type ElicitRequestURLParams,
|
||||
|
|
@ -29,7 +30,6 @@ import {
|
|||
} from "@modelcontextprotocol/sdk/types.js"
|
||||
import { Cause, Effect, Exit, Schema } from "effect"
|
||||
import { ConfigMCP } from "../config/mcp"
|
||||
import { InstallationVersion } from "@opencode-ai/util/installation/version"
|
||||
|
||||
const DEFAULT_STARTUP_TIMEOUT = 30_000
|
||||
const DEFAULT_CATALOG_TIMEOUT = 30_000
|
||||
|
|
@ -185,6 +185,7 @@ export const connect = Effect.fnUntraced(function* (
|
|||
// stored token (and a no-op redirect) surfaces an UnauthorizedError, which we map to needs_auth.
|
||||
authProvider?: OAuthClientProvider,
|
||||
elicitation?: ElicitationHandler,
|
||||
clientInfo: Implementation = { name: "opencode", version: "unknown" },
|
||||
) {
|
||||
const transport: Transport = yield* Effect.gen(function* () {
|
||||
if (config.type === "local") {
|
||||
|
|
@ -209,7 +210,7 @@ export const connect = Effect.fnUntraced(function* (
|
|||
})
|
||||
})
|
||||
const client = new Client(
|
||||
{ name: "opencode", version: InstallationVersion },
|
||||
clientInfo,
|
||||
{
|
||||
capabilities: {
|
||||
...(elicitation ? { elicitation: { form: { applyDefaults: true }, url: {} } } : {}),
|
||||
|
|
|
|||
|
|
@ -157,7 +157,17 @@ export interface Interface {
|
|||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/MCP") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
export const Options = Schema.Struct({
|
||||
clientInfo: Schema.optional(
|
||||
Schema.Struct({
|
||||
name: Schema.String,
|
||||
version: Schema.String,
|
||||
}),
|
||||
),
|
||||
})
|
||||
export type Options = typeof Options.Type
|
||||
|
||||
export const layer = (options?: Options) => Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
|
|
@ -498,7 +508,14 @@ export const layer = Layer.effect(
|
|||
const authProvider = yield* connectProvider(entry)
|
||||
// List tools as part of connect so a failure here marks the server failed rather than
|
||||
// leaving it connected with a silently empty tool list and no path to recover.
|
||||
const result = yield* MCPClient.connect(name, entry.config, location.directory, authProvider, elicitation).pipe(
|
||||
const result = yield* MCPClient.connect(
|
||||
name,
|
||||
entry.config,
|
||||
location.directory,
|
||||
authProvider,
|
||||
elicitation,
|
||||
options?.clientInfo,
|
||||
).pipe(
|
||||
Effect.flatMap((connection) => connection.tools().pipe(Effect.map((tools) => ({ connection, tools })))),
|
||||
Scope.provide(scope),
|
||||
Effect.exit,
|
||||
|
|
@ -772,11 +789,15 @@ export const layer = Layer.effect(
|
|||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [Config.node, Location.node, EventV2.node, Form.node, Integration.node, Credential.node],
|
||||
})
|
||||
export function configured(options?: Options) {
|
||||
return makeLocationNode({
|
||||
service: Service,
|
||||
layer: layer(options),
|
||||
deps: [Config.node, Location.node, EventV2.node, Form.node, Integration.node, Credential.node],
|
||||
})
|
||||
}
|
||||
|
||||
export const node = configured()
|
||||
|
||||
// Schema `optional` strips undefined-valued properties on encode, so fields can assign
|
||||
// optional properties directly instead of conditionally spreading them.
|
||||
|
|
|
|||
|
|
@ -3,12 +3,11 @@ import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effe
|
|||
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
||||
import { ModelsDev } from "@opencode-ai/schema/models-dev"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { Client } from "@opencode-ai/util/client"
|
||||
import { App } from "./app"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Flock } from "@opencode-ai/util/flock"
|
||||
import { Hash } from "@opencode-ai/util/hash"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { InstallationChannel, InstallationVersion } from "@opencode-ai/util/installation/version"
|
||||
import { EventV2 } from "./event"
|
||||
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { httpClient } from "@opencode-ai/util/effect/app-node-platform"
|
||||
|
|
@ -543,7 +542,7 @@ export const layer = (options?: Options) =>
|
|||
Effect.gen(function* () {
|
||||
const fs = yield* FSUtil.Service
|
||||
const events = yield* EventV2.Service
|
||||
const client = yield* Client.Name
|
||||
const app = yield* App.Metadata
|
||||
const http = HttpClient.filterStatusOk(
|
||||
(yield* HttpClient.HttpClient).pipe(
|
||||
HttpClient.retryTransient({
|
||||
|
|
@ -556,7 +555,7 @@ export const layer = (options?: Options) =>
|
|||
|
||||
const source = options?.url || "https://models.dev"
|
||||
const fetch = options?.fetch ?? true
|
||||
const userAgent = `opencode/${InstallationChannel}/${InstallationVersion}/${client}`
|
||||
const userAgent = App.useragent(app)
|
||||
const filepath = path.join(
|
||||
Global.Path.cache,
|
||||
source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`,
|
||||
|
|
@ -660,7 +659,7 @@ export function configured(options?: Options) {
|
|||
return makeGlobalNode({
|
||||
service: Service,
|
||||
layer: layer(options),
|
||||
deps: [FSUtil.node, EventV2.node, Client.node, httpClient],
|
||||
deps: [FSUtil.node, EventV2.node, App.node, httpClient],
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ export * as PluginV2 from "./plugin"
|
|||
import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { Event, ID, type Info } from "@opencode-ai/schema/plugin"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { App } from "./app"
|
||||
import { Context, Effect, Exit, Layer, Scope, Semaphore } from "effect"
|
||||
import { AgentV2 } from "./agent"
|
||||
import { AISDK } from "./aisdk"
|
||||
|
|
@ -144,6 +145,7 @@ export const node = makeLocationNode({
|
|||
layer,
|
||||
deps: [
|
||||
EventV2.node,
|
||||
App.node,
|
||||
AgentV2.node,
|
||||
AISDK.node,
|
||||
Catalog.node,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export * as PluginHost from "./host"
|
|||
|
||||
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import { EventManifest } from "@opencode-ai/schema/event-manifest"
|
||||
import { App } from "../app"
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { AISDK } from "../aisdk"
|
||||
|
|
@ -26,6 +27,7 @@ import { PluginHooks } from "./hooks"
|
|||
|
||||
const mutable = <T>(value: T) => value as DeepMutable<T>
|
||||
export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) {
|
||||
const app = yield* App.Metadata
|
||||
const agents = yield* AgentV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
|
|
@ -61,6 +63,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
|||
effect.pipe(Effect.map((data) => ({ location: locationInfo(), data })))
|
||||
|
||||
return {
|
||||
app,
|
||||
options: {},
|
||||
agent: {
|
||||
get: (id) => agents.get(AgentV2.ID.make(id)),
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ export function fromPromise(plugin: Plugin) {
|
|||
)
|
||||
|
||||
const context2: Context = {
|
||||
app: host.app,
|
||||
options: host.options,
|
||||
agent: {
|
||||
get: (id) => run(host.agent.get(id)),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import os from "os"
|
||||
import { InstallationVersion } from "@opencode-ai/util/installation/version"
|
||||
import { App } from "../../app"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ export const CloudflareAIGatewayPlugin = define({
|
|||
accountId: config.accountId,
|
||||
gateway: config.gatewayId,
|
||||
apiKey: config.apiKey,
|
||||
options: gatewayOptions(evt.options, metadata),
|
||||
options: gatewayOptions(evt.options, metadata, ctx.app),
|
||||
} as any)
|
||||
const unified = createUnified({ apiKey: config.apiKey })
|
||||
evt.sdk = {
|
||||
|
|
@ -64,7 +64,7 @@ function gatewayMetadata(options: Record<string, unknown>) {
|
|||
return raw ? Option.getOrUndefined(decodeJson(raw)) : undefined
|
||||
}
|
||||
|
||||
function gatewayOptions(options: Record<string, unknown>, metadata: unknown) {
|
||||
function gatewayOptions(options: Record<string, unknown>, metadata: unknown, app: App.Info) {
|
||||
return {
|
||||
metadata,
|
||||
cacheTtl: options.cacheTtl,
|
||||
|
|
@ -72,7 +72,7 @@ function gatewayOptions(options: Record<string, unknown>, metadata: unknown) {
|
|||
skipCache: options.skipCache,
|
||||
collectLog: options.collectLog,
|
||||
headers: {
|
||||
"User-Agent": `opencode/${InstallationVersion} cloudflare-ai-gateway (${os.platform()} ${os.release()}; ${os.arch()})`,
|
||||
"User-Agent": `${App.useragent(app)} cloudflare-ai-gateway (${os.platform()} ${os.release()}; ${os.arch()})`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import os from "os"
|
||||
import { InstallationVersion } from "@opencode-ai/util/installation/version"
|
||||
import { App } from "../../app"
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
|
@ -29,10 +29,13 @@ export const CloudflareWorkersAIPlugin = define({
|
|||
if (!hasWorkersEndpoint(evt.model) && !accountId) return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible"))
|
||||
evt.sdk = mod.createOpenAICompatible(
|
||||
sdkOptions({
|
||||
...evt.options,
|
||||
baseURL: evt.options.baseURL ?? (accountId ? workersEndpoint(accountId) : undefined),
|
||||
}) as any,
|
||||
sdkOptions(
|
||||
{
|
||||
...evt.options,
|
||||
baseURL: evt.options.baseURL ?? (accountId ? workersEndpoint(accountId) : undefined),
|
||||
},
|
||||
ctx.app,
|
||||
) as any,
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
|
@ -61,13 +64,13 @@ function hasWorkersEndpoint(model: {
|
|||
return ProviderV2.isAISDK(model.package) && typeof model.settings?.baseURL === "string"
|
||||
}
|
||||
|
||||
function sdkOptions(options: Record<string, any>) {
|
||||
function sdkOptions(options: Record<string, any>, app: App.Info) {
|
||||
return {
|
||||
...options,
|
||||
baseURL: expandAccountId(options.baseURL),
|
||||
apiKey: process.env.CLOUDFLARE_API_KEY ?? options.apiKey,
|
||||
headers: {
|
||||
"User-Agent": `opencode/${InstallationVersion} cloudflare-workers-ai (${os.platform()} ${os.release()}; ${os.arch()})`,
|
||||
"User-Agent": `${App.useragent(app)} cloudflare-workers-ai (${os.platform()} ${os.release()}; ${os.arch()})`,
|
||||
...options.headers,
|
||||
},
|
||||
name: providerID,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { Catalog } from "../../catalog"
|
|||
import { Credential } from "../../credential"
|
||||
import { EventV2 } from "../../event"
|
||||
import { CopilotModels } from "../../github-copilot/models"
|
||||
import { InstallationVersion } from "@opencode-ai/util/installation/version"
|
||||
import { App } from "../../app"
|
||||
import { Integration } from "../../integration"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
|
|
@ -30,7 +30,7 @@ const Token = Schema.Struct({
|
|||
const JsonBody = Schema.UnknownFromJsonString
|
||||
const decodeBody = Schema.decodeUnknownOption(JsonBody)
|
||||
|
||||
const oauth = {
|
||||
const oauth = (app: App.Info) => ({
|
||||
integrationID: Integration.ID.make("github-copilot"),
|
||||
method: {
|
||||
id: methodID,
|
||||
|
|
@ -63,7 +63,7 @@ const oauth = {
|
|||
const urls = oauthURLs(domain)
|
||||
const device = yield* request(urls.device, {
|
||||
method: "POST",
|
||||
headers: headers(),
|
||||
headers: headers(app),
|
||||
body: JSON.stringify({ client_id: clientID, scope: "read:user" }),
|
||||
}).pipe(Effect.map(Schema.decodeUnknownSync(Device)))
|
||||
const interval = Math.max(device.interval, 1) * 1000
|
||||
|
|
@ -71,7 +71,7 @@ const oauth = {
|
|||
const poll = (wait: number): Effect.Effect<Credential.OAuth, unknown> =>
|
||||
request(urls.token, {
|
||||
method: "POST",
|
||||
headers: headers(),
|
||||
headers: headers(app),
|
||||
body: JSON.stringify({
|
||||
client_id: clientID,
|
||||
device_code: device.device_code,
|
||||
|
|
@ -109,7 +109,7 @@ const oauth = {
|
|||
callback: poll(interval),
|
||||
}
|
||||
}),
|
||||
} satisfies IntegrationOAuthMethodRegistration
|
||||
}) satisfies IntegrationOAuthMethodRegistration
|
||||
|
||||
function shouldUseResponses(modelID: string) {
|
||||
// Copilot supports Responses for GPT-5 class models, except mini variants
|
||||
|
|
@ -152,7 +152,7 @@ export const GithubCopilotPlugin = define({
|
|||
{
|
||||
...provider?.headers,
|
||||
Authorization: `Bearer ${credential.refresh}`,
|
||||
"User-Agent": `opencode/${InstallationVersion}`,
|
||||
"User-Agent": App.useragent(ctx.app),
|
||||
"X-GitHub-Api-Version": apiVersion,
|
||||
},
|
||||
existing,
|
||||
|
|
@ -166,7 +166,7 @@ export const GithubCopilotPlugin = define({
|
|||
})
|
||||
|
||||
yield* ctx.integration.transform((draft) => {
|
||||
draft.method.update(oauth)
|
||||
draft.method.update(oauth(ctx.app))
|
||||
})
|
||||
yield* ctx.catalog.transform((evt) => {
|
||||
const item = evt.provider.get(ProviderV2.ID.githubCopilot)
|
||||
|
|
@ -209,6 +209,7 @@ export const GithubCopilotPlugin = define({
|
|||
typeof evt.options.apiKey === "string" ? evt.options.apiKey : undefined,
|
||||
evt.options.fetch,
|
||||
evt.package === "@ai-sdk/anthropic",
|
||||
ctx.app,
|
||||
)
|
||||
if (evt.package === "@ai-sdk/anthropic") {
|
||||
evt.options.headers = {
|
||||
|
|
@ -261,11 +262,11 @@ function baseURL(enterprise?: string) {
|
|||
return enterprise ? `https://copilot-api.${normalizeDomain(enterprise)}` : "https://api.githubcopilot.com"
|
||||
}
|
||||
|
||||
function headers() {
|
||||
function headers(app: App.Info) {
|
||||
return {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": `opencode/${InstallationVersion}`,
|
||||
"User-Agent": App.useragent(app),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -282,7 +283,12 @@ function request(url: string, init: RequestInit) {
|
|||
|
||||
type Fetch = (input: Parameters<typeof fetch>[0], init?: RequestInit) => Promise<Response>
|
||||
|
||||
export function copilotFetch(token: string | undefined, upstream: Fetch | undefined, anthropic: boolean): Fetch {
|
||||
export function copilotFetch(
|
||||
token: string | undefined,
|
||||
upstream: Fetch | undefined,
|
||||
anthropic: boolean,
|
||||
app: App.Info,
|
||||
): Fetch {
|
||||
const send = upstream ?? fetch
|
||||
return async (input, init) => {
|
||||
const requestHeaders = new Headers(init?.headers)
|
||||
|
|
@ -291,7 +297,7 @@ export function copilotFetch(token: string | undefined, upstream: Fetch | undefi
|
|||
requestHeaders.delete("x-api-key")
|
||||
requestHeaders.set("Authorization", `Bearer ${token}`)
|
||||
}
|
||||
requestHeaders.set("User-Agent", `opencode/${InstallationVersion}`)
|
||||
requestHeaders.set("User-Agent", App.useragent(app))
|
||||
requestHeaders.set("Openai-Intent", "conversation-edits")
|
||||
requestHeaders.set("X-GitHub-Api-Version", apiVersion)
|
||||
if (anthropic) requestHeaders.set("anthropic-beta", "interleaved-thinking-2025-05-14")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import os from "os"
|
||||
import { InstallationVersion } from "@opencode-ai/util/installation/version"
|
||||
import { App } from "../../app"
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
|
@ -20,7 +20,7 @@ export const GitLabPlugin = define({
|
|||
: (process.env.GITLAB_INSTANCE_URL ?? "https://gitlab.com"),
|
||||
apiKey: typeof evt.options.apiKey === "string" ? evt.options.apiKey : process.env.GITLAB_TOKEN,
|
||||
aiGatewayHeaders: {
|
||||
"User-Agent": `opencode/${InstallationVersion} gitlab-ai-provider/${mod.VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`,
|
||||
"User-Agent": `${App.useragent(ctx.app)} gitlab-ai-provider/${mod.VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`,
|
||||
"anthropic-beta": "context-1m-2025-08-07",
|
||||
...evt.options.aiGatewayHeaders,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import { createServer } from "node:http"
|
|||
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { Deferred, Effect, Option, Schema, Semaphore, Stream } from "effect"
|
||||
import { App } from "../../app"
|
||||
import { Credential } from "../../credential"
|
||||
import { EventV2 } from "../../event"
|
||||
import { InstallationVersion } from "@opencode-ai/util/installation/version"
|
||||
import { Integration } from "../../integration"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { OauthCallbackPage } from "../../oauth/page"
|
||||
|
|
@ -42,7 +42,7 @@ const Claims = Schema.fromJsonString(
|
|||
)
|
||||
const decodeClaims = Schema.decodeUnknownOption(Claims)
|
||||
|
||||
const browser = {
|
||||
const browser = (app: App.Info) => ({
|
||||
integrationID: Integration.ID.make("openai"),
|
||||
method: {
|
||||
id: browserMethodID,
|
||||
|
|
@ -91,15 +91,15 @@ const browser = {
|
|||
url: authorizeURL(redirect, pkce, state),
|
||||
instructions: "Complete authorization in your browser. This window will close automatically.",
|
||||
callback: Deferred.await(code).pipe(
|
||||
Effect.flatMap((value) => exchange(value, redirect, pkce)),
|
||||
Effect.flatMap((value) => exchange(value, redirect, pkce, app)),
|
||||
Effect.map((tokens) => credential(browserMethodID, tokens)),
|
||||
),
|
||||
}
|
||||
}),
|
||||
refresh: (value) => refresh(browserMethodID, value),
|
||||
} satisfies IntegrationOAuthMethodRegistration
|
||||
refresh: (value) => refresh(browserMethodID, value, app),
|
||||
}) satisfies IntegrationOAuthMethodRegistration
|
||||
|
||||
const headless = {
|
||||
const headless = (app: App.Info) => ({
|
||||
integrationID: Integration.ID.make("openai"),
|
||||
method: {
|
||||
id: headlessMethodID,
|
||||
|
|
@ -112,7 +112,7 @@ const headless = {
|
|||
`${issuer}/api/accounts/deviceauth/usercode`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: headers("application/json"),
|
||||
headers: headers("application/json", app),
|
||||
body: JSON.stringify({ client_id: clientID }),
|
||||
},
|
||||
)
|
||||
|
|
@ -127,7 +127,7 @@ const headless = {
|
|||
try: (signal) =>
|
||||
fetch(`${issuer}/api/accounts/deviceauth/token`, {
|
||||
method: "POST",
|
||||
headers: headers("application/json"),
|
||||
headers: headers("application/json", app),
|
||||
body: JSON.stringify({ device_auth_id: device.device_auth_id, user_code: device.user_code }),
|
||||
signal,
|
||||
}),
|
||||
|
|
@ -140,10 +140,12 @@ const headless = {
|
|||
}
|
||||
return credential(
|
||||
headlessMethodID,
|
||||
yield* exchange(data.authorization_code, `${issuer}/deviceauth/callback`, {
|
||||
verifier: data.code_verifier,
|
||||
challenge: "",
|
||||
}),
|
||||
yield* exchange(
|
||||
data.authorization_code,
|
||||
`${issuer}/deviceauth/callback`,
|
||||
{ verifier: data.code_verifier, challenge: "" },
|
||||
app,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (response.status !== 403 && response.status !== 404) {
|
||||
|
|
@ -154,8 +156,8 @@ const headless = {
|
|||
}),
|
||||
}
|
||||
}),
|
||||
refresh: (value) => refresh(headlessMethodID, value),
|
||||
} satisfies IntegrationOAuthMethodRegistration
|
||||
refresh: (value) => refresh(headlessMethodID, value, app),
|
||||
}) satisfies IntegrationOAuthMethodRegistration
|
||||
|
||||
export const OpenAIPlugin = define({
|
||||
id: "opencode.provider.openai",
|
||||
|
|
@ -173,8 +175,8 @@ export const OpenAIPlugin = define({
|
|||
})
|
||||
|
||||
yield* ctx.integration.transform((draft) => {
|
||||
draft.method.update(browser)
|
||||
draft.method.update(headless)
|
||||
draft.method.update(browser(ctx.app))
|
||||
draft.method.update(headless(ctx.app))
|
||||
})
|
||||
yield* load()
|
||||
yield* ctx.catalog.transform((evt) => {
|
||||
|
|
@ -232,14 +234,14 @@ export const OpenAIPlugin = define({
|
|||
}),
|
||||
} satisfies PluginInternal.InternalPlugin)
|
||||
|
||||
function headers(contentType: string) {
|
||||
return { "Content-Type": contentType, "User-Agent": `opencode/${InstallationVersion}` }
|
||||
function headers(contentType: string, app: App.Info) {
|
||||
return { "Content-Type": contentType, "User-Agent": App.useragent(app) }
|
||||
}
|
||||
|
||||
function exchange(code: string, redirect: string, pkce: Pkce) {
|
||||
function exchange(code: string, redirect: string, pkce: Pkce, app: App.Info) {
|
||||
return request<TokenResponse>(`${issuer}/oauth/token`, {
|
||||
method: "POST",
|
||||
headers: headers("application/x-www-form-urlencoded"),
|
||||
headers: headers("application/x-www-form-urlencoded", app),
|
||||
body: new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
|
|
@ -250,10 +252,10 @@ function exchange(code: string, redirect: string, pkce: Pkce) {
|
|||
})
|
||||
}
|
||||
|
||||
function refresh(methodID: Integration.MethodID, value: Pick<Credential.OAuth, "refresh" | "metadata">) {
|
||||
function refresh(methodID: Integration.MethodID, value: Pick<Credential.OAuth, "refresh" | "metadata">, app: App.Info) {
|
||||
return request<TokenResponse>(`${issuer}/oauth/token`, {
|
||||
method: "POST",
|
||||
headers: headers("application/x-www-form-urlencoded"),
|
||||
headers: headers("application/x-www-form-urlencoded", app),
|
||||
body: new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: value.refresh,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { createServer } from "node:http"
|
|||
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { Clock, Deferred, Effect, Option, Schema } from "effect"
|
||||
import { App } from "../../app"
|
||||
import { Credential } from "../../credential"
|
||||
import { InstallationVersion } from "@opencode-ai/util/installation/version"
|
||||
import { Integration } from "../../integration"
|
||||
import { OauthCallbackPage } from "../../oauth/page"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
|
@ -48,7 +48,7 @@ const DeviceError = Schema.Struct({
|
|||
})
|
||||
const decodeDeviceError = Schema.decodeUnknownOption(Schema.fromJsonString(DeviceError))
|
||||
|
||||
const browser = {
|
||||
const browser = (app: App.Info) => ({
|
||||
integrationID: Integration.ID.make("xai"),
|
||||
method: {
|
||||
id: browserMethodID,
|
||||
|
|
@ -108,15 +108,15 @@ const browser = {
|
|||
url: authorizeURL(pkce, state, randomString(32)),
|
||||
instructions: "Complete authorization in your browser. This window will close automatically.",
|
||||
callback: Deferred.await(code).pipe(
|
||||
Effect.flatMap((value) => exchange(value, pkce)),
|
||||
Effect.flatMap((value) => exchange(value, pkce, app)),
|
||||
Effect.flatMap((tokens) => credential(browserMethodID, tokens)),
|
||||
),
|
||||
}
|
||||
}),
|
||||
refresh: (value) => refresh(browserMethodID, Credential.OAuth.make({ ...value, methodID: browserMethodID })),
|
||||
} satisfies IntegrationOAuthMethodRegistration
|
||||
refresh: (value) => refresh(browserMethodID, Credential.OAuth.make({ ...value, methodID: browserMethodID }), app),
|
||||
}) satisfies IntegrationOAuthMethodRegistration
|
||||
|
||||
const device = {
|
||||
const device = (app: App.Info) => ({
|
||||
integrationID: Integration.ID.make("xai"),
|
||||
method: {
|
||||
id: deviceMethodID,
|
||||
|
|
@ -128,7 +128,7 @@ const device = {
|
|||
`${issuer}/device/code`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: headers(),
|
||||
headers: headers(app),
|
||||
body: new URLSearchParams({ client_id: clientID, scope }).toString(),
|
||||
},
|
||||
Device,
|
||||
|
|
@ -142,14 +142,14 @@ const device = {
|
|||
url: value.verification_uri_complete ?? value.verification_uri,
|
||||
instructions: `Open ${value.verification_uri} on any device and enter code: ${value.user_code}`,
|
||||
...(lifetime ? { expiresAt: created + lifetime * 1000 } : {}),
|
||||
callback: poll(value).pipe(Effect.flatMap((tokens) => credential(deviceMethodID, tokens))),
|
||||
callback: poll(value, app).pipe(Effect.flatMap((tokens) => credential(deviceMethodID, tokens))),
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
refresh: (value) => refresh(deviceMethodID, Credential.OAuth.make({ ...value, methodID: deviceMethodID })),
|
||||
} satisfies IntegrationOAuthMethodRegistration
|
||||
refresh: (value) => refresh(deviceMethodID, Credential.OAuth.make({ ...value, methodID: deviceMethodID }), app),
|
||||
}) satisfies IntegrationOAuthMethodRegistration
|
||||
|
||||
export const XAIPlugin = define({
|
||||
id: "opencode.provider.xai",
|
||||
|
|
@ -158,8 +158,8 @@ export const XAIPlugin = define({
|
|||
draft.update("xai", (integration) => {
|
||||
integration.name = "xAI"
|
||||
})
|
||||
draft.method.update(browser)
|
||||
draft.method.update(device)
|
||||
draft.method.update(browser(ctx.app))
|
||||
draft.method.update(device(ctx.app))
|
||||
draft.method.update({ integrationID: "xai", method: { type: "key", label: "Manually enter API Key" } })
|
||||
})
|
||||
yield* ctx.aisdk.hook(
|
||||
|
|
@ -180,12 +180,12 @@ export const XAIPlugin = define({
|
|||
}),
|
||||
})
|
||||
|
||||
function exchange(code: string, pkce: Pkce) {
|
||||
function exchange(code: string, pkce: Pkce, app: App.Info) {
|
||||
return request(
|
||||
`${issuer}/token`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: headers(),
|
||||
headers: headers(app),
|
||||
body: new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
|
|
@ -198,12 +198,12 @@ function exchange(code: string, pkce: Pkce) {
|
|||
)
|
||||
}
|
||||
|
||||
function refresh(methodID: Integration.MethodID, value: Credential.OAuth) {
|
||||
function refresh(methodID: Integration.MethodID, value: Credential.OAuth, app: App.Info) {
|
||||
return request(
|
||||
`${issuer}/token`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: headers(),
|
||||
headers: headers(app),
|
||||
body: new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: value.refresh,
|
||||
|
|
@ -214,7 +214,7 @@ function refresh(methodID: Integration.MethodID, value: Credential.OAuth) {
|
|||
).pipe(Effect.flatMap((tokens) => credential(methodID, tokens, value.refresh, value.metadata)))
|
||||
}
|
||||
|
||||
function poll(device: typeof Device.Type): Effect.Effect<Token, unknown> {
|
||||
function poll(device: typeof Device.Type, app: App.Info): Effect.Effect<Token, unknown> {
|
||||
return Effect.gen(function* () {
|
||||
const started = yield* Clock.currentTimeMillis
|
||||
const expires = started + positiveSeconds(device.expires_in, 300) * 1000
|
||||
|
|
@ -225,7 +225,7 @@ function poll(device: typeof Device.Type): Effect.Effect<Token, unknown> {
|
|||
}
|
||||
const response = yield* send(`${issuer}/token`, {
|
||||
method: "POST",
|
||||
headers: headers(),
|
||||
headers: headers(app),
|
||||
body: new URLSearchParams({
|
||||
grant_type: deviceGrant,
|
||||
client_id: clientID,
|
||||
|
|
@ -314,11 +314,11 @@ function tokenExpiration(tokens: Token) {
|
|||
return expiration ? expiration * 1000 : Date.now() + 3600 * 1000
|
||||
}
|
||||
|
||||
function headers() {
|
||||
function headers(app: App.Info) {
|
||||
return {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
"User-Agent": `opencode/${InstallationVersion}`,
|
||||
"User-Agent": App.useragent(app),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@
|
|||
|
||||
export * as SkillPlugin from "./skill"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { define, type Context } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { Effect } from "effect"
|
||||
import { AbsolutePath } from "../schema"
|
||||
import { SkillV2 } from "../skill"
|
||||
import { InstallationChannel, InstallationVersion } from "@opencode-ai/util/installation/version"
|
||||
import { Config } from "../config"
|
||||
import { Location } from "../location"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
|
|
@ -27,7 +26,7 @@ const REPORT_DESCRIPTION =
|
|||
export const Plugin = define({
|
||||
id: "opencode.skill",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const reportContent = yield* reportContentWithDiagnostics()
|
||||
const reportContent = yield* reportContentWithDiagnostics(ctx.app)
|
||||
yield* ctx.skill.transform((draft) => {
|
||||
draft.source(
|
||||
SkillV2.EmbeddedSource.make({
|
||||
|
|
@ -58,7 +57,9 @@ export const Plugin = define({
|
|||
}),
|
||||
})
|
||||
|
||||
const reportContentWithDiagnostics = Effect.fn("SkillPlugin.reportContentWithDiagnostics")(function* () {
|
||||
const reportContentWithDiagnostics = Effect.fn("SkillPlugin.reportContentWithDiagnostics")(function* (
|
||||
app: Context["app"],
|
||||
) {
|
||||
const plugins = yield* configuredPlugins().pipe(Effect.orElseSucceed(() => ["Unavailable: failed to inspect config"]))
|
||||
return [
|
||||
ReportContent,
|
||||
|
|
@ -67,8 +68,8 @@ const reportContentWithDiagnostics = Effect.fn("SkillPlugin.reportContentWithDia
|
|||
"",
|
||||
"These values were captured when the built-in report skill was registered. Verify them before publishing.",
|
||||
"",
|
||||
`- opencode version: ${InstallationVersion}`,
|
||||
`- install/channel: ${InstallationChannel}`,
|
||||
`- opencode version: ${app.version}`,
|
||||
`- install/channel: ${app.channel}`,
|
||||
`- OS: ${os.type()} ${os.release()} (${os.platform()} ${os.arch()})`,
|
||||
`- Terminal: ${terminal()}`,
|
||||
`- Shell: ${shell()}`,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { AbsolutePath, PositiveInt, RelativePath } from "./schema"
|
|||
import { AgentV2 } from "./agent"
|
||||
import { SessionV1 } from "./v1/session"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { InstallationVersion } from "@opencode-ai/util/installation/version"
|
||||
import { App } from "./app"
|
||||
import { Slug } from "./util/slug"
|
||||
import { ProjectTable } from "./project/sql"
|
||||
import path from "path"
|
||||
|
|
@ -305,6 +305,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
|||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const app = yield* App.Metadata
|
||||
const database = yield* Database.Service
|
||||
const db = database.db
|
||||
const events = yield* EventV2.Service
|
||||
|
|
@ -352,7 +353,7 @@ const layer = Layer.effect(
|
|||
const info = SessionV1.SessionInfo.make({
|
||||
id: sessionID,
|
||||
slug: Slug.create(),
|
||||
version: InstallationVersion,
|
||||
version: app.version,
|
||||
projectID: project.id,
|
||||
parentID: input.parentID,
|
||||
directory: location.directory,
|
||||
|
|
@ -1040,5 +1041,6 @@ export const node = makeGlobalNode({
|
|||
SessionProjector.node,
|
||||
FSUtil.node,
|
||||
Global.node,
|
||||
App.node,
|
||||
],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { llmClient } from "../effect/app-node-platform"
|
|||
import { SessionEvent } from "./event"
|
||||
import type { SessionMessage } from "./message"
|
||||
import { SessionModelHeaders } from "./model-headers"
|
||||
import { Client } from "@opencode-ai/util/client"
|
||||
import { App } from "../app"
|
||||
import { SessionRunnerModel } from "./runner/model"
|
||||
import { SessionSchema } from "./schema"
|
||||
import { toSessionError } from "./to-session-error"
|
||||
|
|
@ -61,7 +61,7 @@ type Settings = {
|
|||
}
|
||||
|
||||
type Dependencies = {
|
||||
readonly client: string
|
||||
readonly app: App.Info
|
||||
readonly events: EventV2.Interface
|
||||
readonly llm: {
|
||||
readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, LLMError>
|
||||
|
|
@ -260,7 +260,7 @@ const make = (dependencies: Dependencies) => {
|
|||
.stream(
|
||||
LLM.request({
|
||||
model: plan.model,
|
||||
http: { headers: SessionModelHeaders.make(plan.session, dependencies.client) },
|
||||
http: { headers: SessionModelHeaders.make(plan.session, dependencies.app) },
|
||||
messages: [Message.user(plan.prompt)],
|
||||
tools: [],
|
||||
}),
|
||||
|
|
@ -399,13 +399,13 @@ export const layer = Layer.effect(
|
|||
const llm = yield* LLMClient.Service
|
||||
const config = yield* Config.Service
|
||||
const models = yield* SessionRunnerModel.Service
|
||||
const client = yield* Client.Name
|
||||
return make({ events, llm, models, config: settings(yield* config.entries()), client })
|
||||
const app = yield* App.Metadata
|
||||
return make({ events, llm, models, config: settings(yield* config.entries()), app })
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [EventV2.node, llmClient, Config.node, SessionRunnerModel.node, Client.node],
|
||||
deps: [EventV2.node, llmClient, Config.node, SessionRunnerModel.node, App.node],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { LLM, LLMClient, Message, SystemPart } from "@opencode-ai/ai"
|
|||
import { Effect, Layer } from "effect"
|
||||
import { Database } from "../database/database"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { Client } from "@opencode-ai/util/client"
|
||||
import { App } from "../app"
|
||||
import { llmClient } from "../effect/app-node-platform"
|
||||
import { PluginHooks } from "../plugin/hooks"
|
||||
import { SessionContext } from "./context"
|
||||
|
|
@ -23,7 +23,7 @@ export const layer = Layer.effect(
|
|||
const hooks = yield* PluginHooks.Service
|
||||
const llm = yield* LLMClient.Service
|
||||
const models = yield* SessionRunnerModel.Service
|
||||
const client = yield* Client.Name
|
||||
const app = yield* App.Metadata
|
||||
|
||||
return SessionGenerate.Service.of({
|
||||
generate: Effect.fn("SessionGenerate.generate")(function* (input) {
|
||||
|
|
@ -51,7 +51,7 @@ export const layer = Layer.effect(
|
|||
return (yield* llm.generate(
|
||||
LLM.request({
|
||||
model: model.model,
|
||||
http: { headers: SessionModelHeaders.make(selection.session, client) },
|
||||
http: { headers: SessionModelHeaders.make(selection.session, app) },
|
||||
providerOptions: { openai: { promptCacheKey } },
|
||||
system: contextEvent.system,
|
||||
messages: contextEvent.messages,
|
||||
|
|
@ -67,5 +67,5 @@ export const layer = Layer.effect(
|
|||
export const node = makeLocationNode({
|
||||
service: SessionGenerate.Service,
|
||||
layer,
|
||||
deps: [SessionContext.node, Database.node, PluginHooks.node, SessionRunnerModel.node, Client.node, llmClient],
|
||||
deps: [SessionContext.node, Database.node, PluginHooks.node, SessionRunnerModel.node, App.node, llmClient],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
export * as SessionModelHeaders from "./model-headers"
|
||||
|
||||
import { InstallationVersion } from "@opencode-ai/util/installation/version"
|
||||
import { App } from "../app"
|
||||
import { SessionSchema } from "./schema"
|
||||
|
||||
export const make = (session: Pick<SessionSchema.Info, "id" | "parentID" | "projectID">, client: string) => ({
|
||||
export const make = (session: Pick<SessionSchema.Info, "id" | "parentID" | "projectID">, app: App.Info) => ({
|
||||
"x-session-affinity": session.id,
|
||||
"X-Session-Id": session.id,
|
||||
...(session.parentID ? { "x-parent-session-id": session.parentID } : {}),
|
||||
"User-Agent": `opencode/${InstallationVersion}`,
|
||||
"User-Agent": App.useragent(app),
|
||||
"x-opencode-project": session.projectID,
|
||||
"x-opencode-session": session.id,
|
||||
"x-opencode-client": client,
|
||||
"x-opencode-client": app.name,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { LLM, Message, SystemPart, type LLMRequest, type ToolContent } from "@op
|
|||
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { Client } from "@opencode-ai/util/client"
|
||||
import { App } from "../app"
|
||||
import { ModelV2 } from "../model"
|
||||
import { PluginHooks } from "../plugin/hooks"
|
||||
import { ToolRegistry } from "../tool/registry"
|
||||
|
|
@ -85,7 +85,7 @@ export const layer = Layer.effect(
|
|||
Effect.gen(function* () {
|
||||
const hooks = yield* PluginHooks.Service
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const client = yield* Client.Name
|
||||
const app = yield* App.Metadata
|
||||
|
||||
const prepare = Effect.fn("SessionModelRequest.prepare")(function* (input: PrepareInput) {
|
||||
const session = input.context.session
|
||||
|
|
@ -123,7 +123,7 @@ export const layer = Layer.effect(
|
|||
const request = LLM.request({
|
||||
model,
|
||||
http: {
|
||||
headers: SessionModelHeaders.make(session, client),
|
||||
headers: SessionModelHeaders.make(session, app),
|
||||
},
|
||||
providerOptions: { openai: { promptCacheKey } },
|
||||
system: contextEvent.system,
|
||||
|
|
@ -157,5 +157,5 @@ export const layer = Layer.effect(
|
|||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [PluginHooks.node, ToolRegistry.node, Client.node],
|
||||
deps: [PluginHooks.node, ToolRegistry.node, App.node],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { AgentV2 } from "../agent"
|
|||
import { Database } from "../database/database"
|
||||
import { EventV2 } from "../event"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { Client } from "@opencode-ai/util/client"
|
||||
import { App } from "../app"
|
||||
import { llmClient } from "../effect/app-node-platform"
|
||||
import { SessionEvent } from "./event"
|
||||
import { SessionHistory } from "./history"
|
||||
|
|
@ -18,7 +18,7 @@ import { SessionUsage } from "./usage"
|
|||
const MAX_LENGTH = 100
|
||||
|
||||
type Dependencies = {
|
||||
readonly client: string
|
||||
readonly app: App.Info
|
||||
readonly events: EventV2.Interface
|
||||
readonly llm: {
|
||||
readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, LLMError>
|
||||
|
|
@ -68,7 +68,7 @@ const make = (dependencies: Dependencies) => {
|
|||
.stream(
|
||||
LLM.request({
|
||||
model: resolved.model,
|
||||
http: { headers: SessionModelHeaders.make(session, dependencies.client) },
|
||||
http: { headers: SessionModelHeaders.make(session, dependencies.app) },
|
||||
system: agent.system,
|
||||
messages: [Message.user(firstUser.text)],
|
||||
tools: [],
|
||||
|
|
@ -112,8 +112,8 @@ export const layer = Layer.effect(
|
|||
const agents = yield* AgentV2.Service
|
||||
const models = yield* SessionRunnerModel.Service
|
||||
const database = yield* Database.Service
|
||||
const client = yield* Client.Name
|
||||
const title = make({ events, llm, agents, models, client })
|
||||
const app = yield* App.Metadata
|
||||
const title = make({ events, llm, agents, models, app })
|
||||
return Service.of({
|
||||
generateForFirstPrompt: (session) => title.generateForFirstPrompt(database.db, session),
|
||||
})
|
||||
|
|
@ -123,5 +123,5 @@ export const layer = Layer.effect(
|
|||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [EventV2.node, llmClient, AgentV2.node, SessionRunnerModel.node, Database.node, Client.node],
|
||||
deps: [EventV2.node, llmClient, AgentV2.node, SessionRunnerModel.node, Database.node, App.node],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { ToolFailure } from "@opencode-ai/ai"
|
|||
import { Context, Duration, Effect, Layer, Schema } from "effect"
|
||||
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { InstallationVersion } from "@opencode-ai/util/installation/version"
|
||||
import { App } from "../app"
|
||||
import { PositiveInt } from "../schema"
|
||||
import { PermissionV2 } from "../permission"
|
||||
import { Tool } from "./tool"
|
||||
|
|
@ -241,7 +241,7 @@ export const Plugin = {
|
|||
// V2 invocation context does not safely expose the model yet.
|
||||
},
|
||||
{
|
||||
"User-Agent": `opencode/${InstallationVersion}`,
|
||||
"User-Agent": App.useragent(ctx.app),
|
||||
...(config.parallelApiKey ? { Authorization: `Bearer ${config.parallelApiKey}` } : {}),
|
||||
},
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue