🎉 Initial commit: PiAgentUI — AI Agent Desktop & Web UI
Build & Push Frontend Docker Image / build-and-push (push) Canceled after 0s
Build Validation / validate (push) Canceled after 6s
Build & Push Backend Docker Image / build-and-push (push) Failing after 10m10s
Build & Push Gateway Docker Image / build-and-push (push) Failing after 10m9s
Build & Push Frontend Docker Image / build-and-push (push) Canceled after 0s
Build Validation / validate (push) Canceled after 6s
Build & Push Backend Docker Image / build-and-push (push) Failing after 10m10s
Build & Push Gateway Docker Image / build-and-push (push) Failing after 10m9s
A professional workspace for AI coding agents with: - Real-time streaming chat with SSE - Session management (fork, clone, tree navigation) - MCP tools browser & metadata panel - Skills browser (global, project, package sources) - Voice input & STT provider configuration - Integrated terminal & file explorer - Model selector with capability metadata - Keyboard-first UI with command palette - 8 languages (en, pt-BR, es, ja, zh-CN, ar, bn, hi) - Tauri 2 desktop app - React 19 + TypeScript + Vite 8 + Tailwind CSS 4 - Pi Agent extension runtime Originally forked from OpenCodeUI, now an independent project. License: GPL-3.0-only
This commit is contained in:
commit
b3489e1ede
983 files changed
+158475
No files matched your search
@@ -0,0 +1,106 @@
|
||||
export interface OpenRouterModelEntry {
|
||||
id: string
|
||||
name: string
|
||||
description?: string | null
|
||||
context_length?: number | null
|
||||
architecture?: {
|
||||
modality?: string | null
|
||||
input_modalities?: string[] | null
|
||||
output_modalities?: string[] | null
|
||||
tokenizer?: string | null
|
||||
instruct_type?: string | null
|
||||
} | null
|
||||
pricing?: Record<string, string | undefined> | null
|
||||
top_provider?: {
|
||||
context_length?: number | null
|
||||
max_completion_tokens?: number | null
|
||||
is_moderated?: boolean | null
|
||||
} | null
|
||||
supported_parameters?: string[] | null
|
||||
}
|
||||
|
||||
interface ModelQuery {
|
||||
modelID?: string
|
||||
providerID?: string
|
||||
modelName?: string
|
||||
contextWindow?: number
|
||||
}
|
||||
|
||||
function normalize(value: string | undefined): string {
|
||||
return (value ?? '').toLowerCase().replace(/[^a-z0-9]+/g, '')
|
||||
}
|
||||
|
||||
function providerFromOpenRouterId(id: string): string {
|
||||
return id.split('/')[0] ?? ''
|
||||
}
|
||||
|
||||
function isFreeVariant(id: string): boolean {
|
||||
return id.endsWith(':free')
|
||||
}
|
||||
|
||||
function scoreModel(entry: OpenRouterModelEntry, query: ModelQuery): number {
|
||||
const modelID = query.modelID ?? ''
|
||||
const providerID = query.providerID ?? ''
|
||||
const fullID = providerID && modelID ? `${providerID}/${modelID}` : modelID
|
||||
const normalizedEntryID = normalize(entry.id)
|
||||
const normalizedModelID = normalize(modelID)
|
||||
const normalizedFullID = normalize(fullID)
|
||||
const normalizedEntryName = normalize(entry.name)
|
||||
const normalizedModelName = normalize(query.modelName)
|
||||
|
||||
let score = 0
|
||||
|
||||
if (entry.id === fullID) score += 1000
|
||||
if (entry.id === modelID) score += 900
|
||||
if (normalizedEntryID === normalizedFullID) score += 700
|
||||
if (normalizedEntryID === normalizedModelID) score += 650
|
||||
if (modelID && entry.id.endsWith(`/${modelID}`)) score += 600
|
||||
if (normalizedModelID && normalizedEntryID.endsWith(normalizedModelID)) score += 450
|
||||
if (normalizedModelName && normalizedEntryName.includes(normalizedModelName)) score += 250
|
||||
|
||||
const normalizedProvider = normalize(providerID)
|
||||
if (normalizedProvider && normalize(providerFromOpenRouterId(entry.id)) === normalizedProvider) score += 150
|
||||
|
||||
if (query.contextWindow && entry.context_length) {
|
||||
const distance = Math.abs(entry.context_length - query.contextWindow) / query.contextWindow
|
||||
score += Math.max(0, 120 - distance * 120)
|
||||
}
|
||||
|
||||
if (isFreeVariant(entry.id)) score -= 40
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
export function selectOpenRouterModel(
|
||||
catalog: OpenRouterModelEntry[],
|
||||
query: ModelQuery,
|
||||
): OpenRouterModelEntry | undefined {
|
||||
let best: { entry: OpenRouterModelEntry; score: number } | undefined
|
||||
|
||||
for (const entry of catalog) {
|
||||
const score = scoreModel(entry, query)
|
||||
if (score <= 0) continue
|
||||
if (!best || score > best.score) best = { entry, score }
|
||||
}
|
||||
|
||||
return best?.entry
|
||||
}
|
||||
|
||||
export function formatOpenRouterPricePerMillion(value: string | undefined): string {
|
||||
if (value === undefined) return '—'
|
||||
const perToken = Number(value)
|
||||
if (!Number.isFinite(perToken)) return '—'
|
||||
const perMillion = perToken * 1_000_000
|
||||
if (perMillion === 0) return '$0/M'
|
||||
if (perMillion < 0.01) return '<$0.01/M'
|
||||
return `$${perMillion
|
||||
.toFixed(perMillion >= 10 ? 2 : 4)
|
||||
.replace(/0+$/, '')
|
||||
.replace(/\.$/, '')}/M`
|
||||
}
|
||||
|
||||
export function getModelProviderLabel(entry: OpenRouterModelEntry | undefined): string | undefined {
|
||||
if (!entry) return undefined
|
||||
const [provider] = entry.name.split(':')
|
||||
return provider?.trim() || providerFromOpenRouterId(entry.id) || undefined
|
||||
}
|
||||
Reference in new issue
Block a user