Skip to content
Closed
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
85 changes: 73 additions & 12 deletions bun.lock

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions packages/kilo-gateway/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# @opencode-ai/kilo-gateway

Kilo Gateway package for OpenCode providing authentication, AI provider integration, and API access.

## Features

- **Authentication**: Device authorization flow for Kilo Gateway
- **AI Provider**: OpenRouter-based provider with Kilo Gateway integration
- **API Integration**: Profile, balance, and model management
- **TUI Helpers**: Utilities for terminal UI components

## Installation

```bash
bun add @opencode-ai/kilo-gateway
```

## Usage

### Plugin Registration

```typescript
import { KiloAuthPlugin } from "@opencode-ai/kilo-gateway"

// Register with OpenCode
const plugins = [KiloAuthPlugin]
```

### Provider Usage

```typescript
import { createKilo } from "@opencode-ai/kilo-gateway"

const provider = createKilo({
kilocodeToken: process.env.KILO_API_KEY,
kilocodeOrganizationId: "org-123",
})

const model = provider.languageModel("anthropic/claude-sonnet-4")
```

### API Access

```typescript
import { fetchProfile, fetchBalance } from "@opencode-ai/kilo-gateway"

const profile = await fetchProfile(token)
const balance = await fetchBalance(token)
```

## License

MIT
62 changes: 62 additions & 0 deletions packages/kilo-gateway/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/kilo-gateway",
"version": "1.0.21",
"type": "module",
"license": "MIT",
"description": "Kilo Gateway package for OpenCode - authentication, provider, and API integration",
"keywords": [
"kilo",
"opencode",
"auth",
"plugin",
"provider",
"ai",
"llm"
],
"exports": {
".": "./src/index.ts",
"./tui": "./src/tui.ts"
},
"files": [
"dist"
],
"scripts": {
"typecheck": "tsgo --noEmit",
"build": "tsc"
},
"dependencies": {
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.2",
"@clack/prompts": "1.0.0-alpha.1",
"ai": "catalog:",
"open": "10.1.2",
"zod": "catalog:"
},
"devDependencies": {
"@tsconfig/node22": "catalog:",
"@types/node": "catalog:",
"typescript": "catalog:",
"@typescript/native-preview": "catalog:",
"solid-js": "1.9.10",
"@opentui/core": "0.1.75",
"@opentui/solid": "0.1.75"
},
"peerDependencies": {
"solid-js": "*",
"@opentui/core": "*",
"@opentui/solid": "*"
},
"peerDependenciesMeta": {
"solid-js": {
"optional": true
},
"@opentui/core": {
"optional": true
},
"@opentui/solid": {
"optional": true
}
}
}
61 changes: 61 additions & 0 deletions packages/kilo-gateway/src/api/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Kilo Gateway Configuration Constants
* Centralized configuration for all API endpoints, headers, and settings
*/

/** Environment variable for custom Kilo API URL */
export const ENV_KILO_API_URL = "KILO_API_URL"

/** Default Kilo API URL */
export const DEFAULT_KILO_API_URL = "https://api.kilo.ai"

/** Base URL for Kilo API - can be overridden by KILO_API_URL env var */
export const KILO_API_BASE = process.env[ENV_KILO_API_URL] || DEFAULT_KILO_API_URL

/** Default base URL for OpenRouter-compatible endpoint */
export const KILO_OPENROUTER_BASE = `${KILO_API_BASE}/api/openrouter`

/** Device auth polling interval in milliseconds */
export const POLL_INTERVAL_MS = 3000

/** Default model for authenticated users */
export const DEFAULT_MODEL = "anthropic/claude-sonnet-4"

/** Default model for anonymous/free usage */
export const DEFAULT_FREE_MODEL = "minimax/minimax-m2.1:free"

/** Token expiration duration in milliseconds (1 year) */
export const TOKEN_EXPIRATION_MS = 365 * 24 * 60 * 60 * 1000

/** User-Agent header value for requests */
export const USER_AGENT = "opencode-kilo-provider"

/** Content-Type header value for requests */
export const CONTENT_TYPE = "application/json"

/** Default provider name */
export const DEFAULT_PROVIDER_NAME = "kilo"

/** Default API key for anonymous requests */
export const ANONYMOUS_API_KEY = "anonymous"

/** Fetch timeout for model requests in milliseconds (10 seconds) */
export const MODELS_FETCH_TIMEOUT_MS = 10 * 1000

/**
* Header constants for KiloCode API requests
*/
export const HEADER_ORGANIZATIONID = "X-KILOCODE-ORGANIZATIONID"
export const HEADER_TASKID = "X-KILOCODE-TASKID"
export const HEADER_PROJECTID = "X-KILOCODE-PROJECTID"
export const HEADER_TESTER = "X-KILOCODE-TESTER"
export const HEADER_EDITORNAME = "X-KILOCODE-EDITORNAME"

/** Default editor name value */
export const DEFAULT_EDITOR_NAME = "Kilo CLI"

/** Environment variable name for custom editor name */
export const ENV_EDITOR_NAME = "KILOCODE_EDITOR_NAME"

/** Tester header value for suppressing warnings */
export const TESTER_SUPPRESS_VALUE = "SUPPRESS"
216 changes: 216 additions & 0 deletions packages/kilo-gateway/src/api/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { z } from "zod"
import { getKiloUrlFromToken } from "../auth/token.js"
import { DEFAULT_HEADERS } from "../headers.js"
import { KILO_API_BASE, KILO_OPENROUTER_BASE, MODELS_FETCH_TIMEOUT_MS } from "./constants.js"

/**
* OpenRouter model schema
*/
const openRouterArchitectureSchema = z.object({
input_modalities: z.array(z.string()).nullish(),
output_modalities: z.array(z.string()).nullish(),
tokenizer: z.string().nullish(),
})

const openRouterPricingSchema = z.object({
prompt: z.string().nullish(),
completion: z.string().nullish(),
input_cache_write: z.string().nullish(),
input_cache_read: z.string().nullish(),
})

const openRouterModelSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string().optional(),
context_length: z.number(),
max_completion_tokens: z.number().nullish(),
pricing: openRouterPricingSchema.optional(),
architecture: openRouterArchitectureSchema.optional(),
top_provider: z.object({ max_completion_tokens: z.number().nullish() }).optional(),
supported_parameters: z.array(z.string()).optional(),
})

const openRouterModelsResponseSchema = z.object({
data: z.array(openRouterModelSchema),
})

type OpenRouterModel = z.infer<typeof openRouterModelSchema>

/**
* Parse API price string to number (e.g. "0.00001" -> 0.00001)
*/
function parseApiPrice(price: string | null | undefined): number | undefined {
if (!price) return undefined
const parsed = parseFloat(price)
return isNaN(parsed) ? undefined : parsed
}

/**
* Fetch models from Kilo API (OpenRouter-compatible endpoint)
*
* @param options - Configuration options
* @returns Record of models in ModelsDev.Model format
*/
export async function fetchKiloModels(options?: {
kilocodeToken?: string
kilocodeOrganizationId?: string
baseURL?: string
}): Promise<Record<string, any>> {
const token = options?.kilocodeToken
const organizationId = options?.kilocodeOrganizationId

// Construct base URL
const defaultBaseURL = organizationId ? `${KILO_API_BASE}/api/organizations/${organizationId}` : KILO_OPENROUTER_BASE

const baseURL = options?.baseURL ?? defaultBaseURL

// Transform URL with token if available
const finalBaseURL = token ? getKiloUrlFromToken(baseURL, token) : baseURL

// Construct models endpoint
const modelsURL = `${finalBaseURL}/models`

try {
// Fetch models with timeout
const response = await fetch(modelsURL, {
headers: {
...DEFAULT_HEADERS,
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
signal: AbortSignal.timeout(MODELS_FETCH_TIMEOUT_MS),
})

if (!response.ok) {
throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`)
}

const json = await response.json()

// Validate response schema
const result = openRouterModelsResponseSchema.safeParse(json)

if (!result.success) {
console.error("Kilo models response validation failed:", result.error.format())
return {}
}

// Transform models to ModelsDev.Model format
const models: Record<string, any> = {}

for (const model of result.data.data) {
// Skip image generation models
if (model.architecture?.output_modalities?.includes("image")) {
continue
}

const transformedModel = transformToModelDevFormat(model)
models[model.id] = transformedModel
}

return models
} catch (error) {
console.error("Error fetching Kilo models:", error)
return {}
}
}

/**
* Transform OpenRouter model to ModelsDev.Model format
*/
function transformToModelDevFormat(model: OpenRouterModel): any {
const inputModalities = model.architecture?.input_modalities || []
const outputModalities = model.architecture?.output_modalities || []
const supportedParameters = model.supported_parameters || []

// Parse pricing
const inputPrice = parseApiPrice(model.pricing?.prompt)
const outputPrice = parseApiPrice(model.pricing?.completion)
const cacheWritePrice = parseApiPrice(model.pricing?.input_cache_write)
const cacheReadPrice = parseApiPrice(model.pricing?.input_cache_read)

// Determine capabilities
const supportsImages = inputModalities.includes("image")
const supportsTools = supportedParameters.includes("tools")
const supportsReasoning = supportedParameters.includes("reasoning")
const supportsTemperature = supportedParameters.includes("temperature")

// Calculate max output tokens
const maxOutputTokens =
model.top_provider?.max_completion_tokens || model.max_completion_tokens || Math.ceil(model.context_length * 0.2)

return {
id: model.id,
name: model.name,
family: model.id === "kilo/auto" ? "kilo/auto" : extractFamily(model.id), // kilocode_change
release_date: new Date().toISOString().split("T")[0], // Default to today
attachment: supportsImages,
reasoning: supportsReasoning,
temperature: supportsTemperature,
tool_call: supportsTools,
...(inputPrice !== undefined &&
outputPrice !== undefined && {
cost: {
input: inputPrice,
output: outputPrice,
...(cacheReadPrice !== undefined && { cache_read: cacheReadPrice }),
...(cacheWritePrice !== undefined && { cache_write: cacheWritePrice }),
},
}),
limit: {
context: model.context_length,
output: maxOutputTokens,
},
...((inputModalities.length > 0 || outputModalities.length > 0) && {
modalities: {
input: mapModalities(inputModalities),
output: mapModalities(outputModalities),
},
}),
options: {
...(model.description && { description: model.description }),
},
}
}

/**
* Extract family name from model ID
* e.g., "anthropic/claude-3-opus" -> "claude"
*/
function extractFamily(modelId: string): string | undefined {
const parts = modelId.split("/")
if (parts.length < 2) return undefined

const modelName = parts[1]

// Try to extract family from common patterns
if (modelName.includes("claude")) return "claude"
if (modelName.includes("gpt")) return "gpt"
if (modelName.includes("gemini")) return "gemini"
if (modelName.includes("llama")) return "llama"
if (modelName.includes("mistral")) return "mistral"

return undefined
}

/**
* Map OpenRouter modalities to ModelsDev modalities
*/
function mapModalities(modalities: string[]): Array<"text" | "audio" | "image" | "video" | "pdf"> {
const result: Array<"text" | "audio" | "image" | "video" | "pdf"> = []

for (const modality of modalities) {
if (modality === "text") result.push("text")
if (modality === "image") result.push("image")
if (modality === "audio") result.push("audio")
if (modality === "video") result.push("video")
if (modality === "pdf") result.push("pdf")
}

// Always include text if not present
if (!result.includes("text")) {
result.unshift("text")
}

return result
}
Loading
Loading