feat: models

This commit is contained in:
Gab
2026-03-30 18:59:09 +11:00
parent 43048f89c5
commit ffc99307a8
6 changed files with 88 additions and 33 deletions

View File

@@ -381,7 +381,7 @@
}, },
"packages/tfcode": { "packages/tfcode": {
"name": "@toothfairyai/tfcode", "name": "@toothfairyai/tfcode",
"version": "1.0.22", "version": "1.0.23",
"bin": { "bin": {
"tfcode": "./bin/tfcode", "tfcode": "./bin/tfcode",
}, },

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.0.22", "version": "1.0.23",
"name": "@toothfairyai/tfcode", "name": "@toothfairyai/tfcode",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",

View File

@@ -1,5 +1,5 @@
import { Prompt, type PromptRef } from "@tui/component/prompt" import { Prompt, type PromptRef } from "@tui/component/prompt"
import { createEffect, createMemo, Match, on, onMount, Show, Switch } from "solid-js" import { createEffect, createMemo, Match, on, onMount, Show, Switch, createSignal } from "solid-js"
import { useTheme } from "@tui/context/theme" import { useTheme } from "@tui/context/theme"
import { useKeybind } from "@tui/context/keybind" import { useKeybind } from "@tui/context/keybind"
import { Logo } from "../component/logo" import { Logo } from "../component/logo"
@@ -15,6 +15,10 @@ import { Installation } from "@/installation"
import { useKV } from "../context/kv" import { useKV } from "../context/kv"
import { useCommandDialog } from "../component/dialog-command" import { useCommandDialog } from "../component/dialog-command"
import { useLocal } from "../context/local" import { useLocal } from "../context/local"
import { useSDK } from "../context/sdk"
import semver from "semver"
import { TextAttributes } from "@opentui/core"
import { Flag } from "@/flag/flag"
// TODO: what is the best way to do this? // TODO: what is the best way to do this?
let once = false let once = false
@@ -26,6 +30,7 @@ export function Home() {
const route = useRouteData("home") const route = useRouteData("home")
const promptRef = usePromptRef() const promptRef = usePromptRef()
const command = useCommandDialog() const command = useCommandDialog()
const sdk = useSDK()
const mcp = createMemo(() => Object.keys(sync.data.mcp).length > 0) const mcp = createMemo(() => Object.keys(sync.data.mcp).length > 0)
const mcpError = createMemo(() => { const mcpError = createMemo(() => {
return Object.values(sync.data.mcp).some((x) => x.status === "failed") return Object.values(sync.data.mcp).some((x) => x.status === "failed")
@@ -43,6 +48,19 @@ export function Home() {
return !tipsHidden() return !tipsHidden()
}) })
const [updateAvailable, setUpdateAvailable] = createSignal<string | null>(null)
const skippedVersion = createMemo(() => kv.get("skipped_version"))
onMount(() => {
const unsub = sdk.event.on("installation.update-available", (evt) => {
const version = evt.properties.version
const skipped = skippedVersion()
if (skipped && !semver.gt(version, skipped)) return
setUpdateAvailable(version)
})
return unsub
})
command.register(() => [ command.register(() => [
{ {
title: tipsHidden() ? "Show tips" : "Hide tips", title: tipsHidden() ? "Show tips" : "Hide tips",
@@ -132,6 +150,27 @@ export function Home() {
<box flexGrow={1} minHeight={0} /> <box flexGrow={1} minHeight={0} />
<Toast /> <Toast />
</box> </box>
<Show when={updateAvailable()}>
<box
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
paddingRight={2}
flexDirection="row"
flexShrink={0}
gap={2}
alignItems="center"
>
<box backgroundColor={theme.warning} paddingLeft={1} paddingRight={1} paddingTop={0} paddingBottom={0}>
<text fg={theme.background} attributes={TextAttributes.BOLD}>
!
</text>
</box>
<text fg={theme.text}>Update available: v{updateAvailable()}</text>
<box flexGrow={1} />
<text fg={theme.textMuted}>/changelog</text>
</box>
</Show>
<box paddingTop={1} paddingBottom={1} paddingLeft={2} paddingRight={2} flexDirection="row" flexShrink={0} gap={2}> <box paddingTop={1} paddingBottom={1} paddingLeft={2} paddingRight={2} flexDirection="row" flexShrink={0} gap={2}>
<text fg={theme.textMuted}>{directory()}</text> <text fg={theme.textMuted}>{directory()}</text>
<box gap={1} flexDirection="row" flexShrink={0}> <box gap={1} flexDirection="row" flexShrink={0}>

View File

@@ -15,6 +15,7 @@ export async function upgrade() {
} }
if (Installation.VERSION === latest) return if (Installation.VERSION === latest) return
if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) return if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) return
const kind = Installation.getReleaseType(Installation.VERSION, latest) const kind = Installation.getReleaseType(Installation.VERSION, latest)

View File

@@ -62,7 +62,7 @@ export namespace Installation {
export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local" export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local"
export const CHANNEL = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local" export const CHANNEL = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local"
export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}/${Flag.OPENCODE_CLIENT}` export const USER_AGENT = `tfcode/${CHANNEL}/${VERSION}/${Flag.OPENCODE_CLIENT}`
export function isPreview() { export function isPreview() {
return CHANNEL !== "latest" return CHANNEL !== "latest"
@@ -141,16 +141,16 @@ export namespace Installation {
) )
const getBrewFormula = Effect.fnUntraced(function* () { const getBrewFormula = Effect.fnUntraced(function* () {
const tapFormula = yield* text(["brew", "list", "--formula", "anomalyco/tap/opencode"]) const tapFormula = yield* text(["brew", "list", "--formula", "toothfairyai/tap/tfcode"])
if (tapFormula.includes("opencode")) return "anomalyco/tap/opencode" if (tapFormula.includes("tfcode")) return "toothfairyai/tap/tfcode"
const coreFormula = yield* text(["brew", "list", "--formula", "opencode"]) const coreFormula = yield* text(["brew", "list", "--formula", "tfcode"])
if (coreFormula.includes("opencode")) return "opencode" if (coreFormula.includes("tfcode")) return "tfcode"
return "opencode" return "tfcode"
}) })
const upgradeCurl = Effect.fnUntraced( const upgradeCurl = Effect.fnUntraced(
function* (target: string) { function* (target: string) {
const response = yield* httpOk.execute(HttpClientRequest.get("https://opencode.ai/install")) const response = yield* httpOk.execute(HttpClientRequest.get("https://toothfairyai.com/install"))
const body = yield* response.text const body = yield* response.text
const bodyBytes = new TextEncoder().encode(body) const bodyBytes = new TextEncoder().encode(body)
const proc = ChildProcess.make("bash", [], { const proc = ChildProcess.make("bash", [], {
@@ -180,9 +180,9 @@ export namespace Installation {
{ name: "yarn", command: () => text(["yarn", "global", "list"]) }, { name: "yarn", command: () => text(["yarn", "global", "list"]) },
{ name: "pnpm", command: () => text(["pnpm", "list", "-g", "--depth=0"]) }, { name: "pnpm", command: () => text(["pnpm", "list", "-g", "--depth=0"]) },
{ name: "bun", command: () => text(["bun", "pm", "ls", "-g"]) }, { name: "bun", command: () => text(["bun", "pm", "ls", "-g"]) },
{ name: "brew", command: () => text(["brew", "list", "--formula", "opencode"]) }, { name: "brew", command: () => text(["brew", "list", "--formula", "tfcode"]) },
{ name: "scoop", command: () => text(["scoop", "list", "opencode"]) }, { name: "scoop", command: () => text(["scoop", "list", "tfcode"]) },
{ name: "choco", command: () => text(["choco", "list", "--limit-output", "opencode"]) }, { name: "choco", command: () => text(["choco", "list", "--limit-output", "tfcode"]) },
] ]
checks.sort((a, b) => { checks.sort((a, b) => {
@@ -196,7 +196,9 @@ export namespace Installation {
for (const check of checks) { for (const check of checks) {
const output = yield* check.command() const output = yield* check.command()
const installedName = const installedName =
check.name === "brew" || check.name === "choco" || check.name === "scoop" ? "opencode" : "opencode-ai" check.name === "brew" || check.name === "choco" || check.name === "scoop"
? "tfcode"
: "@toothfairyai/tfcode"
if (output.includes(installedName)) { if (output.includes(installedName)) {
return check.name return check.name
} }
@@ -216,7 +218,7 @@ export namespace Installation {
return info.formulae[0].versions.stable return info.formulae[0].versions.stable
} }
const response = yield* httpOk.execute( const response = yield* httpOk.execute(
HttpClientRequest.get("https://formulae.brew.sh/api/formula/opencode.json").pipe( HttpClientRequest.get("https://formulae.brew.sh/api/formula/tfcode.json").pipe(
HttpClientRequest.acceptJson, HttpClientRequest.acceptJson,
), ),
) )
@@ -230,7 +232,7 @@ export namespace Installation {
const registry = reg.endsWith("/") ? reg.slice(0, -1) : reg const registry = reg.endsWith("/") ? reg.slice(0, -1) : reg
const channel = CHANNEL const channel = CHANNEL
const response = yield* httpOk.execute( const response = yield* httpOk.execute(
HttpClientRequest.get(`${registry}/opencode-ai/${channel}`).pipe(HttpClientRequest.acceptJson), HttpClientRequest.get(`${registry}/@toothfairyai/tfcode/${channel}`).pipe(HttpClientRequest.acceptJson),
) )
const data = yield* HttpClientResponse.schemaBodyJson(NpmPackage)(response) const data = yield* HttpClientResponse.schemaBodyJson(NpmPackage)(response)
return data.version return data.version
@@ -239,7 +241,7 @@ export namespace Installation {
if (detectedMethod === "choco") { if (detectedMethod === "choco") {
const response = yield* httpOk.execute( const response = yield* httpOk.execute(
HttpClientRequest.get( HttpClientRequest.get(
"https://community.chocolatey.org/api/v2/Packages?$filter=Id%20eq%20%27opencode%27%20and%20IsLatestVersion&$select=Version", "https://community.chocolatey.org/api/v2/Packages?$filter=Id%20eq%20%27tfcode%27%20and%20IsLatestVersion&$select=Version",
).pipe(HttpClientRequest.setHeaders({ Accept: "application/json;odata=verbose" })), ).pipe(HttpClientRequest.setHeaders({ Accept: "application/json;odata=verbose" })),
) )
const data = yield* HttpClientResponse.schemaBodyJson(ChocoPackage)(response) const data = yield* HttpClientResponse.schemaBodyJson(ChocoPackage)(response)
@@ -249,7 +251,7 @@ export namespace Installation {
if (detectedMethod === "scoop") { if (detectedMethod === "scoop") {
const response = yield* httpOk.execute( const response = yield* httpOk.execute(
HttpClientRequest.get( HttpClientRequest.get(
"https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/opencode.json", "https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/tfcode.json",
).pipe(HttpClientRequest.setHeaders({ Accept: "application/json" })), ).pipe(HttpClientRequest.setHeaders({ Accept: "application/json" })),
) )
const data = yield* HttpClientResponse.schemaBodyJson(ScoopManifest)(response) const data = yield* HttpClientResponse.schemaBodyJson(ScoopManifest)(response)
@@ -257,7 +259,7 @@ export namespace Installation {
} }
const response = yield* httpOk.execute( const response = yield* httpOk.execute(
HttpClientRequest.get("https://api.github.com/repos/anomalyco/opencode/releases/latest").pipe( HttpClientRequest.get("https://api.github.com/repos/ToothFairyAI/tfcode/releases/latest").pipe(
HttpClientRequest.acceptJson, HttpClientRequest.acceptJson,
), ),
) )
@@ -272,24 +274,24 @@ export namespace Installation {
result = yield* upgradeCurl(target) result = yield* upgradeCurl(target)
break break
case "npm": case "npm":
result = yield* run(["npm", "install", "-g", `opencode-ai@${target}`]) result = yield* run(["npm", "install", "-g", `@toothfairyai/tfcode@${target}`])
break break
case "pnpm": case "pnpm":
result = yield* run(["pnpm", "install", "-g", `opencode-ai@${target}`]) result = yield* run(["pnpm", "install", "-g", `@toothfairyai/tfcode@${target}`])
break break
case "bun": case "bun":
result = yield* run(["bun", "install", "-g", `opencode-ai@${target}`]) result = yield* run(["bun", "install", "-g", `@toothfairyai/tfcode@${target}`])
break break
case "brew": { case "brew": {
const formula = yield* getBrewFormula() const formula = yield* getBrewFormula()
const env = { HOMEBREW_NO_AUTO_UPDATE: "1" } const env = { HOMEBREW_NO_AUTO_UPDATE: "1" }
if (formula.includes("/")) { if (formula.includes("/")) {
const tap = yield* run(["brew", "tap", "anomalyco/tap"], { env }) const tap = yield* run(["brew", "tap", "toothfairyai/tap"], { env })
if (tap.code !== 0) { if (tap.code !== 0) {
result = tap result = tap
break break
} }
const repo = yield* text(["brew", "--repo", "anomalyco/tap"]) const repo = yield* text(["brew", "--repo", "toothfairyai/tap"])
const dir = repo.trim() const dir = repo.trim()
if (dir) { if (dir) {
const pull = yield* run(["git", "pull", "--ff-only"], { cwd: dir, env }) const pull = yield* run(["git", "pull", "--ff-only"], { cwd: dir, env })
@@ -303,10 +305,10 @@ export namespace Installation {
break break
} }
case "choco": case "choco":
result = yield* run(["choco", "upgrade", "opencode", `--version=${target}`, "-y"]) result = yield* run(["choco", "upgrade", "tfcode", `--version=${target}`, "-y"])
break break
case "scoop": case "scoop":
result = yield* run(["scoop", "install", `opencode@${target}`]) result = yield* run(["scoop", "install", `tfcode@${target}`])
break break
default: default:
return yield* new UpgradeFailedError({ stderr: `Unknown method: ${m}` }) return yield* new UpgradeFailedError({ stderr: `Unknown method: ${m}` })

View File

@@ -7,7 +7,11 @@ import type { OpenAICompatibleChatPrompt } from "./openai-compatible-api-types"
import { convertToBase64 } from "@ai-sdk/provider-utils" import { convertToBase64 } from "@ai-sdk/provider-utils"
function getOpenAIMetadata(message: { providerOptions?: SharedV2ProviderMetadata }) { function getOpenAIMetadata(message: { providerOptions?: SharedV2ProviderMetadata }) {
return message?.providerOptions?.copilot ?? {} const opts = message?.providerOptions ?? {}
return {
...(opts.openaiCompatible as Record<string, unknown> | undefined),
...(opts.copilot as Record<string, unknown> | undefined),
}
} }
export function convertToOpenAICompatibleChatMessages(prompt: LanguageModelV2Prompt): OpenAICompatibleChatPrompt { export function convertToOpenAICompatibleChatMessages(prompt: LanguageModelV2Prompt): OpenAICompatibleChatPrompt {
@@ -82,7 +86,6 @@ export function convertToOpenAICompatibleChatMessages(prompt: LanguageModelV2Pro
for (const part of content) { for (const part of content) {
const partMetadata = getOpenAIMetadata(part) const partMetadata = getOpenAIMetadata(part)
// Check for reasoningOpaque on any part (may be attached to text/tool-call)
const partOpaque = (part.providerOptions as { copilot?: { reasoningOpaque?: string } })?.copilot const partOpaque = (part.providerOptions as { copilot?: { reasoningOpaque?: string } })?.copilot
?.reasoningOpaque ?.reasoningOpaque
if (partOpaque && !reasoningOpaque) { if (partOpaque && !reasoningOpaque) {
@@ -113,15 +116,25 @@ export function convertToOpenAICompatibleChatMessages(prompt: LanguageModelV2Pro
} }
} }
messages.push({ const msg: any = {
role: "assistant", role: "assistant",
content: text || undefined, content: text || undefined,
tool_calls: toolCalls.length > 0 ? toolCalls : undefined, tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
reasoning_text: reasoningOpaque ? reasoningText : undefined,
reasoning_opaque: reasoningOpaque,
reasoning_content: reasoningText,
...metadata, ...metadata,
}) }
if (metadata?.reasoning_content) {
msg.reasoning_content = metadata.reasoning_content
} else if (reasoningText) {
msg.reasoning_content = reasoningText
}
if (reasoningOpaque) {
msg.reasoning_opaque = reasoningOpaque
if (reasoningText) msg.reasoning_text = reasoningText
}
messages.push(msg)
break break
} }