Skip to content
Open
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
837 changes: 416 additions & 421 deletions bun.lock

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion packages/app/src/components/session-context-usage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const metrics = createMemo(() => getSessionContextMetrics(messages(), providers.all()))
const context = createMemo(() => metrics().context)
const cost = createMemo(() => {
const quota = sync.data.provider_quota
if (quota && metrics().context?.message.providerID === "kiro")
return `${quota.subscriptionTitle}: ${quota.currentUsage.toLocaleString()}/${quota.usageLimit.toLocaleString()} credits`
return usd().format(metrics().totalCost)
})

Expand Down Expand Up @@ -96,7 +99,9 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
</Show>
<div class="flex items-center gap-2">
<span class="text-text-invert-strong">{cost()}</span>
<span class="text-text-invert-base">{language.t("context.usage.cost")}</span>
<Show when={!(sync.data.provider_quota && metrics().context?.message.providerID === "kiro")}>
<span class="text-text-invert-base">{language.t("context.usage.cost")}</span>
</Show>
</div>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ export function SessionContextTab() {
const formatter = createMemo(() => createSessionContextFormatter(language.intl()))

const cost = createMemo(() => {
const quota = sync.data.provider_quota
if (quota && metrics().context?.message.providerID === "kiro")
return `${quota.subscriptionTitle}: ${quota.currentUsage.toLocaleString()}/${quota.usageLimit.toLocaleString()} credits`
return usd().format(metrics().totalCost)
})

Expand Down Expand Up @@ -213,7 +216,7 @@ export function SessionContextTab() {
},
{ label: "context.stats.userMessages", value: () => counts().user.toLocaleString(language.intl()) },
{ label: "context.stats.assistantMessages", value: () => counts().assistant.toLocaleString(language.intl()) },
{ label: "context.stats.totalCost", value: cost },
{ label: sync.data.provider_quota && metrics().context?.message.providerID === "kiro" ? "context.stats.subscription" : "context.stats.totalCost", value: cost },
{ label: "context.stats.sessionCreated", value: () => formatter().time(info()?.time.created) },
{ label: "context.stats.lastActivity", value: () => formatter().time(ctx()?.message.time.created) },
] satisfies { label: string; value: () => JSX.Element }[]
Expand Down
8 changes: 8 additions & 0 deletions packages/app/src/context/global-sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,14 @@ function createGlobalSync() {
})
},
})
if (event.type === "session.status") {
const props = event.properties as { status: { type: string } }
if (props.status.type === "idle")
void sdkFor(directory)
.global.provider.quota()
.then((x) => x.data && setStore("provider_quota", x.data))
.catch(() => {})
}
})

onCleanup(unsub)
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/context/global-sync/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,4 +357,7 @@ export async function bootstrapDirectory(input: {
description: formatServerError(err, input.translate),
})
})
void retry(() => input.sdk.global.provider.quota())
.then((x) => x.data && input.setStore("provider_quota", x.data))
.catch(() => {})
}
1 change: 1 addition & 0 deletions packages/app/src/context/global-sync/child-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export function createChildStoreManager(input: {
lsp_ready: false,
lsp: [],
vcs: vcsStore.value,
provider_quota: undefined,
limit: 5,
message: {},
part: {},
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/context/global-sync/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export type State = {
lsp_ready: boolean
lsp: LspStatus[]
vcs: VcsInfo | undefined
provider_quota: { currentUsage: number; usageLimit: number; subscriptionTitle: string } | undefined
limit: number
message: {
[sessionID: string]: Message[]
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ export const dict = {
"context.stats.userMessages": "User Messages",
"context.stats.assistantMessages": "Assistant Messages",
"context.stats.totalCost": "Total Cost",
"context.stats.subscription": "Subscription",
"context.stats.sessionCreated": "Session Created",
"context.stats.lastActivity": "Last Activity",

Expand Down
1 change: 1 addition & 0 deletions packages/opencode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
"ignore": "7.0.5",
"immer": "11.1.4",
"jsonc-parser": "3.3.1",
"kiro-ai-provider": "0.4.4",
"mime-types": "3.0.2",
"minimatch": "10.0.3",
"npm-package-arg": "13.0.2",
Expand Down
7 changes: 6 additions & 1 deletion packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,14 @@ export function Prompt(props: PromptProps) {
const model = sync.data.provider.find((item) => item.id === last.providerID)?.models[last.modelID]
const pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined
const cost = msg.reduce((sum, item) => sum + (item.role === "assistant" ? item.cost : 0), 0)

return {
context: pct ? `${Locale.number(tokens)} (${pct})` : Locale.number(tokens),
cost: cost > 0 ? money.format(cost) : undefined,
cost: sync.data.provider_quota && last.providerID === "kiro"
? `${sync.data.provider_quota.subscriptionTitle}: ${sync.data.provider_quota.currentUsage.toLocaleString()}/${sync.data.provider_quota.usageLimit.toLocaleString()} credits`
: cost > 0
? money.format(cost)
: undefined,
}
})

Expand Down
11 changes: 11 additions & 0 deletions packages/opencode/src/cli/cmd/tui/context/sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
[key: string]: McpResource
}
formatter: FormatterStatus[]
provider_quota: { currentUsage: number; usageLimit: number; subscriptionTitle: string } | undefined
vcs: VcsInfo | undefined
}>({
provider_next: {
Expand Down Expand Up @@ -101,6 +102,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
mcp: {},
mcp_resource: {},
formatter: [],
provider_quota: undefined,
vcs: undefined,
})

Expand Down Expand Up @@ -225,6 +227,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({

case "session.status": {
setStore("session_status", event.properties.sessionID, event.properties.status)
if (event.properties.status.type === "idle")
sdk.client.global.provider
.quota()
.then((x) => x.data && setStore("provider_quota", x.data))
.catch(() => {})
break
}

Expand Down Expand Up @@ -431,6 +438,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
sdk.client.provider.auth({ workspace }).then((x) => setStore("provider_auth", reconcile(x.data ?? {}))),
sdk.client.vcs.get({ workspace }).then((x) => setStore("vcs", reconcile(x.data))),
project.workspace.sync(),
sdk.client.global.provider
.quota()
.then((x) => x.data && setStore("provider_quota", x.data))
.catch(() => {}),
]).then(() => {
setStore("status", "complete")
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { AssistantMessage } from "@opencode-ai/sdk/v2"
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
import { createMemo } from "solid-js"
import { createMemo, createSignal } from "solid-js"

const id = "internal:sidebar-context"

Expand All @@ -13,19 +13,28 @@ function View(props: { api: TuiPluginApi; session_id: string }) {
const theme = () => props.api.theme.current
const msg = createMemo(() => props.api.state.session.messages(props.session_id))
const cost = createMemo(() => msg().reduce((sum, item) => sum + (item.role === "assistant" ? item.cost : 0), 0))
const [quota, setQuota] = createSignal<{ currentUsage: number; usageLimit: number; subscriptionTitle: string } | undefined>()
props.api.client.global.provider
.quota()
.then((x) => x.data && setQuota(x.data))
.catch(() => {})

const last = createMemo(() =>
msg().findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0),
)

const state = createMemo(() => {
const last = msg().findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0)
if (!last) {
const l = last()
if (!l) {
return {
tokens: 0,
percent: null,
}
}

const tokens =
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
const model = props.api.state.provider.find((item) => item.id === last.providerID)?.models[last.modelID]
l.tokens.input + l.tokens.output + l.tokens.reasoning + l.tokens.cache.read + l.tokens.cache.write
const model = props.api.state.provider.find((item) => item.id === l.providerID)?.models[l.modelID]
return {
tokens,
percent: model?.limit.context ? Math.round((tokens / model.limit.context) * 100) : null,
Expand All @@ -39,7 +48,12 @@ function View(props: { api: TuiPluginApi; session_id: string }) {
</text>
<text fg={theme().textMuted}>{state().tokens.toLocaleString()} tokens</text>
<text fg={theme().textMuted}>{state().percent ?? 0}% used</text>
<text fg={theme().textMuted}>{money.format(cost())} spent</text>
<text fg={theme().textMuted}>
{quota() && last()?.providerID === "kiro"
? `${quota()!.subscriptionTitle}: ${quota()!.currentUsage.toLocaleString()}/${quota()!.usageLimit.toLocaleString()} credits`
: money.format(cost())}{" "}
spent
</text>
</box>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ export function SubagentFooter() {

return {
context: pct ? `${Locale.number(tokens)} (${pct})` : Locale.number(tokens),
cost: cost > 0 ? money.format(cost) : undefined,
cost: sync.data.provider_quota && last.providerID === "kiro"
? `${sync.data.provider_quota.subscriptionTitle}: ${sync.data.provider_quota.currentUsage.toLocaleString()}/${sync.data.provider_quota.usageLimit.toLocaleString()} credits`
: cost > 0
? money.format(cost)
: undefined,
}
})

Expand Down
4 changes: 3 additions & 1 deletion packages/opencode/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { CodexAuthPlugin } from "./codex"
import { Session } from "../session"
import { NamedError } from "@opencode-ai/util/error"
import { CopilotAuthPlugin } from "./github-copilot/copilot"
import { KiroAuthPlugin } from "./kiro"
import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth"
import { PoeAuthPlugin } from "opencode-poe-auth"
import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cloudflare"
Expand Down Expand Up @@ -55,13 +56,14 @@ export namespace Plugin {
export class Service extends Context.Service<Service, Interface>()("@opencode/Plugin") {}

// Built-in plugins that are directly imported (not installed from npm)
const INTERNAL_PLUGINS: PluginInstance[] = [
const INTERNAL_PLUGINS: PluginInstance[] = [
CodexAuthPlugin,
CopilotAuthPlugin,
GitlabAuthPlugin,
PoeAuthPlugin,
CloudflareWorkersAuthPlugin,
CloudflareAIGatewayAuthPlugin,
KiroAuthPlugin,
]

function isServerPlugin(value: unknown): value is PluginInstance {
Expand Down
127 changes: 127 additions & 0 deletions packages/opencode/src/plugin/kiro.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
import { authenticate, getToken } from "kiro-ai-provider"

const BUILDER_ID_URL = "https://view.awsapps.com/start"
const USER_AGENT = "aws-sdk-js/1.0.27 ua/2.1 os/darwin lang/js api/codewhispererstreaming#1.0.27 m/E Kiro-opencode"
const USER_AGENT_SHORT = "aws-sdk-js/1.0.27 Kiro-opencode"

export async function KiroAuthPlugin(_input: PluginInput): Promise<Hooks> {
return {
auth: {
provider: "kiro",
async loader(getAuth) {
const info = await getAuth()
if (!info || info.type !== "oauth") return {}

return {
async fetch(request: RequestInfo | URL, init?: RequestInit) {
const token = info.access || (await getToken())
if (!token) return fetch(request, init)

return fetch(request, {
...init,
headers: {
...(init?.headers as Record<string, string>),
Authorization: `Bearer ${token}`,
...(token.startsWith("ksk_") ? { tokentype: "API_KEY" } : {}),
"User-Agent": USER_AGENT,
"x-amz-user-agent": USER_AGENT_SHORT,
"x-amzn-codewhisperer-optout": "true",
},
})
},
}
},
methods: [
{
type: "oauth" as const,
label: "Login with Kiro",
prompts: [
{
type: "select" as const,
key: "authType",
message: "Select Kiro authentication type",
options: [
{ label: "AWS Builder ID", value: "builder-id", hint: "Free" },
{ label: "IAM Identity Center", value: "idc", hint: "Enterprise" },
{ label: "API Key", value: "apikey", hint: "Pro, Pro+, Power" },
],
},
{
type: "text" as const,
key: "startUrl",
message: "Enter your SSO start URL (defaults to $AWS_SSO_START_URL if set)",
placeholder: "https://d-xxxxxxxxxx.awsapps.com/start",
when: { key: "authType", op: "eq" as const, value: "idc" },
},
{
type: "text" as const,
key: "region",
message: "Enter your AWS SSO region (defaults to $AWS_SSO_REGION if set)",
placeholder: "us-east-1",
when: { key: "authType", op: "eq" as const, value: "idc" },
},
{
type: "text" as const,
key: "apiKey",
message: "Enter your Kiro API key",
placeholder: "ksk-...",
when: { key: "authType", op: "eq" as const, value: "apikey" },
},
],
async authorize(inputs = {} as Record<string, string>) {
if (inputs.authType === "apikey") {
const key = inputs.apiKey
return {
url: "",
instructions: "API key saved",
method: "auto" as const,
callback: async () => {
if (!key) return { type: "failed" as const }
return {
type: "success" as const,
refresh: "",
access: key,
expires: Date.now() + 365 * 24 * 60 * 60 * 1000,
}
},
}
}

const url =
inputs.authType === "idc" ? (inputs.startUrl || process.env.AWS_SSO_START_URL) : BUILDER_ID_URL
const region =
inputs.authType === "idc"
? (inputs.region || process.env.AWS_SSO_REGION || "us-east-1")
: "us-east-1"

const { promise: pending, resolve } = Promise.withResolvers<{ url: string; code: string }>()

const auth = authenticate({
startUrl: url,
region,
onVerification: (verify, code) => resolve({ url: verify, code }),
})

const verification = await pending

return {
url: verification.url,
instructions: verification.code,
method: "auto" as const,
callback: () =>
auth
.then((result) => ({
type: "success" as const,
refresh: result.refreshToken,
access: result.accessToken,
expires: Date.now() + 3600000,
}))
.catch(() => ({ type: "failed" as const })),
}
},
},
],
},
}
}
Loading
Loading