Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ BrowserCode supports any model you can reach with an API key, plus [every provid

Use `/connect` in the TUI, or set provider API keys in your environment.

For GPT-6 Astra, set `OPENAI_API_KEY` and select `openai/gpt-6-astra`:

```bash
bcode run -m openai/gpt-6-astra --variant high "Your task"
```

Supported reasoning variants: `low`, `medium`, `high`, `xhigh`, `max` (default: `medium`).
Astra uses the Responses API for tool calls. This requires a build containing Astra support;
the published `0.1.20` binary predates it.

Recommended models from current BU Bench evals:

- Best performance: `claude-opus-4-8`
Expand Down
38 changes: 37 additions & 1 deletion packages/core/src/models-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,30 @@ export const Model = Schema.Struct({
})
export type Model = Schema.Schema.Type<typeof Model>

// Temporary fallback until models.dev lists Astra. Never replace catalog metadata.
// https://developers.openai.com/api/docs/models/gpt-6-astra
// https://developers.openai.com/api/docs/pricing
const astra: Model = {
id: "gpt-6-astra",
name: "GPT-6 Astra",
family: "gpt-6",
release_date: "",
attachment: true,
reasoning: true,
temperature: false,
tool_call: true,
reasoning_options: [{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }],
modalities: { input: ["text", "image"], output: ["text"] },
limit: { context: 1050000, input: 922000, output: 128000 },
cost: {
input: 10,
output: 50,
cache_read: 1,
cache_write: 12.5,
tiers: [{ tier: { type: "context", size: 272000 }, input: 20, output: 75, cache_read: 2, cache_write: 25 }],
},
}

export const Provider = Schema.Struct({
api: Schema.optional(Schema.String),
name: Schema.String,
Expand Down Expand Up @@ -222,7 +246,19 @@ const layer = Layer.effect(
}),
)
return JSON.parse(text) as Record<string, Provider>
}).pipe(Effect.withSpan("ModelsDev.populate"), Effect.orDie)
}).pipe(
Effect.map((catalog) => {
if (source !== "https://models.dev" || Flag.OPENCODE_MODELS_PATH) return catalog
const openai = catalog.openai
if (!openai || openai.models[astra.id]) return catalog
return {
...catalog,
openai: { ...openai, models: { ...openai.models, [astra.id]: astra } },
}
}),
Effect.withSpan("ModelsDev.populate"),
Effect.orDie,
)

const [cachedGet, invalidate] = yield* Effect.cachedInvalidateWithTTL(populate, Duration.infinity)

Expand Down
74 changes: 74 additions & 0 deletions packages/core/test/models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,80 @@ const initialState: MockState = {
}

describe("ModelsDev Service", () => {
it.live("fills missing Astra metadata without changing the cached catalog", () =>
Effect.gen(function* () {
const catalog = { ...fixture, openai: { id: "openai", name: "OpenAI", env: ["OPENAI_API_KEY"], models: {} } }
yield* writeCache(catalog)
const state = yield* Ref.make(initialState)
const result = yield* provided(
state,
ModelsDev.Service.use((s) => s.get()),
)
expect(result.acme).toEqual(fixture.acme)
expect(result.openai.models["gpt-6-astra"]).toMatchObject({
reasoning: true,
temperature: false,
tool_call: true,
reasoning_options: [{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }],
limit: { context: 1050000, input: 922000, output: 128000 },
cost: {
input: 10,
output: 50,
cache_read: 1,
cache_write: 12.5,
tiers: [{ tier: { type: "context", size: 272000 }, input: 20, output: 75, cache_read: 2, cache_write: 25 }],
},
})
expect(JSON.parse(yield* Effect.promise(() => readFile(cacheFile, "utf8")))).toEqual(catalog)
expect((yield* Ref.get(state)).calls).toEqual([])
}),
)

it.live("prefers Astra metadata from the catalog over the fallback", () =>
Effect.gen(function* () {
const catalog = {
openai: {
id: "openai",
name: "OpenAI",
env: [],
models: {
"gpt-6-astra": { ...fixture.acme.models["acme-1"], id: "gpt-6-astra", name: "Catalog Astra" },
},
},
}
yield* writeCache(catalog)
const state = yield* Ref.make(initialState)
expect(
yield* provided(
state,
ModelsDev.Service.use((s) => s.get()),
),
).toEqual(catalog)
}),
)

it.live("does not inject Astra into an explicitly configured catalog file", () =>
Effect.gen(function* () {
const catalog = { openai: { id: "openai", name: "OpenAI", env: [], models: {} } }
yield* writeCache(catalog)
const state = yield* Ref.make(initialState)
yield* Effect.acquireUseRelease(
Effect.sync(() => {
Flag.OPENCODE_MODELS_PATH = cacheFile
}),
() =>
provided(
state,
ModelsDev.Service.use((s) => s.get()),
).pipe(Effect.tap((result) => Effect.sync(() => expect(result).toEqual(catalog)))),
() =>
Effect.sync(() => {
Flag.OPENCODE_MODELS_PATH = undefined
}),
)
}),
)

it.live("get() returns providers from disk when cache file exists", () =>
Effect.gen(function* () {
yield* writeCache(fixture)
Expand Down
7 changes: 5 additions & 2 deletions packages/llm/src/protocols/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ const OpenAIResponsesCoreFields = {
include: optionalArray(OpenAIOptions.OpenAIResponseIncludable),
reasoning: Schema.optional(
Schema.Struct({
effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
effort: Schema.optional(Schema.Union([OpenAIOptions.OpenAIReasoningEffort, Schema.Literal("max")])),
summary: Schema.optional(Schema.Literal("auto")),
}),
),
Expand Down Expand Up @@ -457,7 +457,10 @@ const lowerOptions = Effect.fn("OpenAIResponses.lowerOptions")(function* (reques
const store = OpenAIOptions.store(request)
const promptCacheKey = OpenAIOptions.promptCacheKey(request)
const effort = OpenAIOptions.reasoningEffort(request)
if (effort && !OpenAIOptions.isReasoningEffort(effort))
const astra = request.model.id === "gpt-6-astra"
if (astra && (effort === "none" || effort === "minimal"))
return yield* invalid(`GPT-6 Astra does not support reasoning effort ${effort}; use low or higher`)
if (effort && !OpenAIOptions.isReasoningEffort(effort) && !(astra && effort === "max"))
return yield* invalid(`OpenAI Responses does not support reasoning effort ${effort}`)
const summary = OpenAIOptions.reasoningSummary(request)
const include = OpenAIOptions.include(request)
Expand Down
38 changes: 38 additions & 0 deletions packages/llm/test/provider/openai-responses.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,44 @@ const expectToolOutput = (body: OpenAIResponses.OpenAIResponsesBody): OpenAITool
}

describe("OpenAI Responses route", () => {
for (const effort of ["low", "medium", "high", "xhigh", "max"] as const) {
it.effect(`prepares Astra reasoning effort ${effort} without sending a request`, () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({
model: OpenAI.configure({ apiKey: "test", baseURL: "https://api.openai.test/v1" }).responses("gpt-6-astra"),
prompt: "hi",
providerOptions: {
openai: { reasoningEffort: effort, reasoningSummary: "auto", include: ["reasoning.encrypted_content"] },
},
}),
)
expect(prepared.body.reasoning).toEqual({ effort, summary: "auto" })
expect(prepared.body.store).toBe(false)
expect(prepared.body.include).toEqual(["reasoning.encrypted_content"])
}),
)
}

for (const [id, effort] of [
["gpt-6-astra", "none"],
["gpt-6-astra", "minimal"],
["gpt-5.5", "max"],
] as const) {
it.effect(`rejects unsupported ${id} effort ${effort} before sending`, () =>
Effect.gen(function* () {
const result = yield* LLMClient.prepare(
LLM.request({
model: OpenAI.configure({ apiKey: "test" }).responses(id),
prompt: "hi",
providerOptions: { openai: { reasoningEffort: effort } },
}),
).pipe(Effect.result)
expect(result._tag).toBe("Failure")
}),
)
}

it.effect("prepares OpenAI Responses target", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(request)
Expand Down
7 changes: 7 additions & 0 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,7 @@ function gpt5ChatReasoningEfforts(apiId: string) {
// to strongest.
function openaiReasoningEfforts(apiId: string, releaseDate: string) {
const id = apiId.toLowerCase()
if (id === "gpt-6-astra") return ["low", "medium", "high", "xhigh", "max"]
if (id.includes("deep-research")) return ["medium"]
const chatEfforts = gpt5ChatReasoningEfforts(id)
if (chatEfforts) return chatEfforts
Expand Down Expand Up @@ -1224,6 +1225,12 @@ export function options(input: {
return result
}

if (input.model.api.npm === "@ai-sdk/openai" && input.model.api.id === "gpt-6-astra") {
result["reasoningEffort"] = "medium"
result["reasoningSummary"] = "auto"
result["include"] = INCLUDE_ENCRYPTED_REASONING
}

if (input.model.api.id.includes("gpt-5") && !input.model.api.id.includes("gpt-5-chat")) {
if (!input.model.api.id.includes("gpt-5-pro")) {
result["reasoningEffort"] = "medium"
Expand Down
124 changes: 124 additions & 0 deletions packages/opencode/test/provider/astra.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { describe, expect, test } from "bun:test"
import { createOpenAI } from "@ai-sdk/openai"
import { generateText, jsonSchema, stepCountIs, tool } from "ai"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { Provider } from "../../src/provider/provider"
import { ProviderTransform } from "../../src/provider/transform"

const model: Provider.Model = {
id: ModelV2.ID.make("astra-alias"),
providerID: ProviderV2.ID.make("openai"),
api: { id: "gpt-6-astra", url: "https://api.openai.test/v1", npm: "@ai-sdk/openai" },
name: "Astra",
capabilities: {
temperature: false,
reasoning: true,
attachment: true,
toolcall: true,
interleaved: false,
input: { text: true, image: true, audio: false, video: false, pdf: false },
output: { text: true, image: false, audio: false, video: false, pdf: false },
},
cost: { input: 10, output: 50, cache: { read: 1, write: 12.5 } },
limit: { context: 1050000, input: 922000, output: 128000 },
status: "active",
options: {},
headers: {},
release_date: "",
}

describe("GPT-6 Astra", () => {
test("uses the API ID for the five supported variants and valid defaults", () => {
const variants = ProviderTransform.variants(model)
expect(Object.keys(variants)).toEqual(["low", "medium", "high", "xhigh", "max"])
expect(ProviderTransform.options({ model, sessionID: "test", providerOptions: {} })).toMatchObject({
store: false,
reasoningEffort: "medium",
reasoningSummary: "auto",
include: ["reasoning.encrypted_content"],
})
expect(ProviderTransform.smallOptions({ ...model, variants })).toMatchObject({ reasoningEffort: "low" })
})

for (const effort of ["low", "medium", "high", "xhigh", "max"]) {
test(`${effort} reaches Responses and replays encrypted reasoning through a tool round trip`, async () => {
const requests: Array<{ url: string; body: Record<string, unknown> }> = []
const sdk = createOpenAI({
apiKey: "offline-test-key",
baseURL: model.api.url,
fetch: Object.assign(
async (url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
requests.push({ url: String(url), body: JSON.parse(String(init?.body)) })
return Response.json({
id: `resp_${requests.length}`,
created_at: 0,
model: model.api.id,
output:
requests.length === 1
? [
{
type: "reasoning",
id: "rs_1",
summary: [{ type: "summary_text", text: "Checking." }],
encrypted_content: "test-encrypted-state",
},
{ type: "function_call", id: "fc_1", call_id: "call_1", name: "lookup", arguments: "{}" },
]
: [
{
type: "message",
id: "msg_1",
role: "assistant",
content: [{ type: "output_text", text: "Done.", annotations: [] }],
},
],
usage: { input_tokens: 20, output_tokens: 10, total_tokens: 30 },
})
},
{ preconnect() {} },
),
})
const defaults = ProviderTransform.options({ model, sessionID: "test", providerOptions: {} })
const result = await generateText({
model: sdk.responses(model.api.id),
prompt: "Call lookup.",
tools: {
lookup: tool({
inputSchema: jsonSchema<Record<string, never>>({ type: "object", properties: {} }),
execute: async () => "found",
}),
},
stopWhen: stepCountIs(2),
maxRetries: 0,
// Even explicitly configured sampling settings must not reach Astra.
temperature: 0.5,
topP: 0.9,
providerOptions: ProviderTransform.providerOptions(model, {
...defaults,
...ProviderTransform.variants(model)[effort],
}),
})
expect(result.text).toBe("Done.")
expect(requests).toHaveLength(2)
for (const request of requests) {
expect(request.url).toBe("https://api.openai.test/v1/responses")
expect(request.body).toMatchObject({
model: "gpt-6-astra",
store: false,
reasoning: { effort, summary: "auto" },
include: ["reasoning.encrypted_content"],
})
expect(request.body).not.toHaveProperty("temperature")
expect(request.body).not.toHaveProperty("top_p")
}
expect(requests[1].body.input).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: "reasoning", encrypted_content: "test-encrypted-state" }),
expect.objectContaining({ type: "function_call", call_id: "call_1", name: "lookup" }),
expect.objectContaining({ type: "function_call_output", call_id: "call_1", output: "found" }),
]),
)
})
}
})
Loading