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
20 changes: 18 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/opencode-mini/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
GITHUB_TOKEN_ALICE=
GITHUB_TOKEN_BOB=
136 changes: 136 additions & 0 deletions packages/opencode-mini/examples/basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/**
* opencode-mini usage examples
*
* Run with: bun run packages/opencode-mini/examples/basic.ts
*/
import { create, tool, Session } from "../src/index"

// ---------------------------------------------------------------------------
// Create an instance with copilot enabled
// ---------------------------------------------------------------------------

const mini = create({ directory: process.cwd(), copilot: {} })
await mini.init()

// ---------------------------------------------------------------------------
// Multi-tenant copilot: each user brings their own GitHub OAuth token
// ---------------------------------------------------------------------------

mini.credentials.set("user-alice", {
providerID: "copilot",
token: "gho_alice-github-oauth-token",
})

mini.credentials.set("user-bob", {
providerID: "copilot",
token: "gho_bob-github-oauth-token",
})

const session = await mini.session.create({ title: "Shared session" })

// Alice sends a message routed through copilot with her token
await mini.prompt({
sessionID: session.id,
parts: [{ type: "text", text: "Hello from Alice" }],
userId: "user-alice",
model: { providerID: "copilot", modelID: "claude-sonnet-4-20250514" },
})

// Bob continues the same conversation with his own token
await mini.prompt({
sessionID: session.id,
parts: [{ type: "text", text: "Hello from Bob" }],
userId: "user-bob",
model: { providerID: "copilot", modelID: "gpt-4o" },
})

// Retrieve conversation history
const messages = await mini.session.messages(session.id)
for (const msg of messages) {
console.log(`[${msg.info.role}]`, msg.parts.length, "parts")
}

// Remove credentials when a user logs out
mini.credentials.remove("user-alice")

// ---------------------------------------------------------------------------
// API-key providers work too (Anthropic, OpenAI, etc.)
// ---------------------------------------------------------------------------

mini.credentials.set("user-carol", {
providerID: "anthropic",
token: "sk-ant-carol-key",
})

await mini.prompt({
sessionID: session.id,
parts: [{ type: "text", text: "Hello from Carol" }],
userId: "user-carol",
model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" },
})

// ---------------------------------------------------------------------------
// Custom copilot model list
// ---------------------------------------------------------------------------

const custom = create({
directory: process.cwd(),
copilot: {
provider: "my-copilot",
models: {
"claude-sonnet-4-20250514": { name: "Claude Sonnet 4", limit: { context: 200000, output: 16384 } },
"gpt-4o": { name: "GPT-4o", limit: { context: 128000, output: 16384 } },
},
},
})

// ---------------------------------------------------------------------------
// Custom tools
// ---------------------------------------------------------------------------

await mini.tools.register(
"current_time",
tool({
description: "Returns the current date and time",
args: {},
async execute() {
return new Date().toISOString()
},
}),
)

// ---------------------------------------------------------------------------
// Events
// ---------------------------------------------------------------------------

const unsub = await mini.subscribeAll((event) => {
console.log("Event:", event.type)
})

await mini.subscribe(Session.Event.Created, (event) => {
console.log("Session created:", event.properties.info.id)
})

unsub()

// ---------------------------------------------------------------------------
// Cancel
// ---------------------------------------------------------------------------

const long = await mini.session.create({ title: "Cancellable" })

const pending = mini.prompt({
sessionID: long.id,
parts: [{ type: "text", text: "Write a very long essay about the history of computing" }],
userId: "user-bob",
model: { providerID: "copilot", modelID: "gpt-4o" },
})

setTimeout(() => mini.cancel(long.id), 2000)
await pending.catch(() => console.log("Prompt cancelled"))

// ---------------------------------------------------------------------------
// Cleanup
// ---------------------------------------------------------------------------

await mini.dispose()
53 changes: 53 additions & 0 deletions packages/opencode-mini/examples/sample.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { create, tool, Session } from "../src/index"
import { env } from "bun"

const mini = create({ directory: process.cwd(), copilot: {}, logLevel: "ERROR" })
await mini.init()

mini.credentials.set("user-alice", {
providerID: "copilot",
token: env.GITHUB_TOKEN_ALICE ?? "",
})

mini.credentials.set("user-bob", {
providerID: "copilot",
token: env.GITHUB_TOKEN_BOB ?? "",
})

console.log("[!] Creating session...")
const session = await mini.session.create({ title: "Shared session" })

console.log("[!] Alice sends a message...")
let replyA1 = await mini.prompt({
sessionID: session.id,
parts: [{ type: "text", text: "Hello from Alice" }],
userId: "user-alice",
model: { providerID: "copilot", modelID: "claude-opus-4.6" },
})
console.log(replyA1)

console.log("[!] Bob sends a message...")
let replyB1 = await mini.prompt({
sessionID: session.id,
parts: [{ type: "text", text: "Hello from Bob" }],
userId: "user-bob",
model: { providerID: "copilot", modelID: "claude-opus-4.6" },
})
console.log(replyB1)

console.log("[!] Alice asks who's in the conversation...")
let replyA2 = await mini.prompt({
sessionID: session.id,
parts: [{ type: "text", text: "Who is part of this conversation?" }],
userId: "user-alice",
model: { providerID: "copilot", modelID: "claude-opus-4.6" },
})
console.log(replyA2)

// console.log("[!] Retrieving conversation history...")
// const messages = await mini.session.messages(session.id)
// for (const msg of messages) {
// console.log(`[${msg.info.role}]`, msg)
// }

await mini.dispose()
23 changes: 23 additions & 0 deletions packages/opencode-mini/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/opencode-mini",
"version": "0.0.1",
"type": "module",
"license": "MIT",
"scripts": {
"typecheck": "tsgo --noEmit"
},
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"opencode": "workspace:*",
"@opencode-ai/plugin": "workspace:*"
},
"devDependencies": {
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
"typescript": "catalog:"
}
}
85 changes: 85 additions & 0 deletions packages/opencode-mini/src/copilot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Installation } from "opencode/installation"
import { ModelsDev } from "opencode/provider/models"

const TOKEN_HEADER = "x-copilot-token"
const BASE_URL = "https://api.githubcopilot.com"

function detect(url: string, init?: RequestInit) {
try {
const body = typeof init?.body === "string" ? JSON.parse(init.body) : init?.body

// Completions API
if (body?.messages && url.includes("completions")) {
const last = body.messages[body.messages.length - 1]
return {
vision: body.messages.some(
(msg: any) => Array.isArray(msg.content) && msg.content.some((part: any) => part.type === "image_url"),
),
agent: last?.role !== "user",
}
}

// Responses API
if (body?.input) {
const last = body.input[body.input.length - 1]
return {
vision: body.input.some(
(item: any) => Array.isArray(item?.content) && item.content.some((part: any) => part.type === "input_image"),
),
agent: last?.role !== "user",
}
}

// Messages API
if (body?.messages) {
const last = body.messages[body.messages.length - 1]
const hasNonTool = Array.isArray(last?.content) && last.content.some((part: any) => part?.type !== "tool_result")
return {
vision: body.messages.some(
(item: any) =>
Array.isArray(item?.content) &&
item.content.some(
(part: any) =>
part?.type === "image" ||
(part?.type === "tool_result" &&
Array.isArray(part?.content) &&
part.content.some((nested: any) => nested?.type === "image")),
),
),
agent: !(last?.role === "user" && hasNonTool),
}
}
} catch {}
return { vision: false, agent: false }
}

export async function copilotFetch(request: RequestInfo | URL, init?: RequestInit) {
const incoming = (init?.headers ?? {}) as Record<string, string>
const token = incoming[TOKEN_HEADER]
if (!token) return fetch(request, init)

const url = request instanceof URL ? request.href : request.toString()
const { vision, agent } = detect(url, init)

const headers: Record<string, string> = {
"x-initiator": agent ? "agent" : "user",
...incoming,
"User-Agent": `opencode/${Installation.VERSION}`,
Authorization: `Bearer ${token}`,
"Openai-Intent": "conversation-edits",
}
if (vision) headers["Copilot-Vision-Request"] = "true"

delete headers[TOKEN_HEADER]
delete headers["x-api-key"]
delete headers["authorization"]

return fetch(request, { ...init, headers })
}

export { TOKEN_HEADER, BASE_URL }

export async function models() {
const data = await ModelsDev.get()
return data["github-copilot"]?.models ?? {}
}
Loading