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
9 changes: 9 additions & 0 deletions dashboard/src/api/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ export async function updateLlmConfig(config: LlmConfig): Promise<RuntimeConfig>
apiKey: toSecretPayload(provider.apiKey ?? null),
apiType: normalizeLlmApiType(provider.apiType),
legacyApi: provider.legacyApi === true ? true : null,
sourceUrl: provider.sourceUrl,
gonkaAddress: provider.gonkaAddress,
endpoints: provider.endpoints,
},
]),
),
Expand All @@ -154,6 +157,9 @@ export async function addLlmProvider(name: string, config: LlmProviderConfig): P
apiKey: toSecretPayload(config.apiKey ?? null),
apiType: normalizeLlmApiType(config.apiType),
legacyApi: config.legacyApi === true ? true : null,
sourceUrl: config.sourceUrl,
gonkaAddress: config.gonkaAddress,
endpoints: config.endpoints,
};
const { data } = await client.post<RuntimeConfigUiRecord>(
`/settings/runtime/llm/providers/${name}`,
Expand All @@ -170,6 +176,9 @@ export async function updateLlmProvider(name: string, config: LlmProviderConfig)
apiKey: toSecretPayload(config.apiKey ?? null),
apiType: normalizeLlmApiType(config.apiType),
legacyApi: config.legacyApi === true ? true : null,
sourceUrl: config.sourceUrl,
gonkaAddress: config.gonkaAddress,
endpoints: config.endpoints,
};
const { data } = await client.put<RuntimeConfigUiRecord>(
`/settings/runtime/llm/providers/${name}`,
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/api/settingsApiUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function toSecretPayload(value: string | null | undefined): SecretPayload
return { value, encrypted: false };
}

const SUPPORTED_LLM_API_TYPES = ['openai', 'anthropic', 'gemini'] as const;
const SUPPORTED_LLM_API_TYPES = ['openai', 'anthropic', 'gemini', 'gonka'] as const;
type SupportedLlmApiType = (typeof SUPPORTED_LLM_API_TYPES)[number];

function isSupportedLlmApiType(value: string): value is SupportedLlmApiType {
Expand Down
23 changes: 22 additions & 1 deletion dashboard/src/api/settingsRuntimeMappers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { toModelRegistryConfig, toModelRouterConfig } from './settingsModelMappers';
import { toSelfEvolvingConfig } from './settingsSelfEvolvingMappers';
import type { RuntimeConfig } from './settingsTypes';
import type { GonkaEndpointConfig, RuntimeConfig } from './settingsTypes';
import { hasSecretValue, type UnknownRecord } from './settingsUtils';
import { normalizeLlmApiType, toSecretPayload } from './settingsApiUtils';

Expand Down Expand Up @@ -70,6 +70,9 @@ function normalizeLlmProviders(providers: Record<string, UnknownRecord>): Record
apiKeyPresent: hasSecretValue(provider.apiKey as UnknownRecord | undefined),
apiType: normalizeLlmApiType(provider.apiType),
legacyApi: provider.legacyApi === true ? true : null,
sourceUrl: typeof provider.sourceUrl === 'string' ? provider.sourceUrl : null,
gonkaAddress: typeof provider.gonkaAddress === 'string' ? provider.gonkaAddress : null,
endpoints: toGonkaEndpoints(provider.endpoints),
}]));
}

Expand All @@ -95,9 +98,27 @@ function toBackendLlmProviders(providers: RuntimeConfig['llm']['providers']): Un
apiKey: toSecretPayload(provider.apiKey ?? null),
apiType: normalizeLlmApiType(provider.apiType),
legacyApi: provider.legacyApi === true ? true : null,
sourceUrl: provider.sourceUrl,
gonkaAddress: provider.gonkaAddress,
endpoints: provider.endpoints,
}]));
}

function toGonkaEndpoints(value: unknown): GonkaEndpointConfig[] {
if (!Array.isArray(value)) {
return [];
}
return value.flatMap((entry) => {
if (entry == null || typeof entry !== 'object') {
return [];
}
const record = entry as UnknownRecord;
const url = typeof record.url === 'string' ? record.url.trim() : '';
const transferAddress = typeof record.transferAddress === 'string' ? record.transferAddress.trim() : '';
return url.length > 0 && transferAddress.length > 0 ? [{ url, transferAddress }] : [];
});
}

function toBackendSelfEvolvingConfig(selfEvolving: UnknownRecord): UnknownRecord {
const tactics = selfEvolving.tactics as UnknownRecord | undefined;
const search = tactics?.search as UnknownRecord | undefined;
Expand Down
5 changes: 3 additions & 2 deletions dashboard/src/api/settingsTypes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ExplicitModelTierId } from '../lib/modelTiers';

export type ApiType = 'openai' | 'anthropic' | 'gemini';
export type ApiType = 'openai' | 'anthropic' | 'gemini' | 'gonka';

export interface RuntimeConfig {
telegram: TelegramConfig;
Expand All @@ -27,7 +27,8 @@ export interface RuntimeConfig {

export interface ModelRegistryConfig { repositoryUrl: string | null; branch: string | null; }
export interface LlmConfig { providers: Record<string, LlmProviderConfig>; }
export interface LlmProviderConfig { apiKey: string | null; apiKeyPresent?: boolean; baseUrl: string | null; requestTimeoutSeconds: number | null; apiType: ApiType | null; legacyApi: boolean | null; }
export interface GonkaEndpointConfig { url: string; transferAddress: string; }
export interface LlmProviderConfig { apiKey: string | null; apiKeyPresent?: boolean; baseUrl: string | null; requestTimeoutSeconds: number | null; apiType: ApiType | null; legacyApi: boolean | null; sourceUrl: string | null; gonkaAddress: string | null; endpoints: GonkaEndpointConfig[]; }
export interface MemoryConfig { enabled: boolean | null; softPromptBudgetTokens: number | null; maxPromptBudgetTokens: number | null; workingTopK: number | null; episodicTopK: number | null; semanticTopK: number | null; proceduralTopK: number | null; promotionEnabled: boolean | null; promotionMinConfidence: number | null; decayEnabled: boolean | null; decayDays: number | null; retrievalLookbackDays: number | null; codeAwareExtractionEnabled: boolean | null; disclosure?: MemoryDisclosureConfig | null; diagnostics?: MemoryDiagnosticsConfig | null; }
export interface MemoryPreset { id: string; label: string; comment: string; memory: MemoryConfig; }
export type MemoryDisclosureMode = 'index' | 'summary' | 'selective_detail' | 'full_pack';
Expand Down
10 changes: 6 additions & 4 deletions dashboard/src/pages/settings/LlmProviderBaseUrlField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function LlmProviderBaseUrlField({ name, form, onFormChange }: LlmProvide
const hasBaseUrl = (form.baseUrl ?? '').trim().length > 0;
const suggestedBaseUrl = getSuggestedBaseUrl(name, apiType);
const shouldShowUseDefaultBaseUrl = suggestedBaseUrl != null && form.baseUrl !== suggestedBaseUrl;
const shouldShowClearBaseUrl = apiType === 'gemini' && hasBaseUrl;
const shouldShowClearBaseUrl = (apiType === 'gemini' || apiType === 'gonka') && hasBaseUrl;

return (
<Form.Group className="mb-2">
Expand All @@ -41,9 +41,11 @@ export function LlmProviderBaseUrlField({ name, form, onFormChange }: LlmProvide
<Form.Text className="text-body-secondary">
{apiType === 'gemini'
? 'Gemini uses the native Google endpoint, so Base URL is usually left empty.'
: suggestedBaseUrl != null
? `Recommended endpoint: ${suggestedBaseUrl}`
: 'Leave empty to use the provider default endpoint.'}
: apiType === 'gonka'
? 'Optional: direct /v1 endpoint for model discovery. Runtime requests use Gonka Source URL or endpoints below.'
: suggestedBaseUrl != null
? `Recommended endpoint: ${suggestedBaseUrl}`
: 'Leave empty to use the provider default endpoint.'}
</Form.Text>
</Form.Group>
);
Expand Down
4 changes: 4 additions & 0 deletions dashboard/src/pages/settings/LlmProviderEditorCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from './llmProvidersSupport';
import { LlmProviderBaseUrlField } from './LlmProviderBaseUrlField';
import { LlmProviderSecretField } from './LlmProviderSecretField';
import { LlmProviderGonkaFields } from './LlmProviderGonkaFields';

export interface LlmProviderEditorCardProps {
name: string;
Expand Down Expand Up @@ -79,6 +80,9 @@ export function LlmProviderEditorCard({
</Form.Text>
</Form.Group>
</Col>
{apiType === 'gonka' && (
<LlmProviderGonkaFields form={form} onFormChange={onFormChange} />
)}
{apiType === 'openai' && (
<Col md={3}>
<Form.Group className="mb-2">
Expand Down
97 changes: 97 additions & 0 deletions dashboard/src/pages/settings/LlmProviderGonkaFields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { ReactElement } from 'react';
import { Button, Col, Form, Row } from 'react-bootstrap';

import type { LlmProviderConfig } from '../../api/settingsTypes';
import { toNullableString } from './llmProvidersSupport';

export interface LlmProviderGonkaFieldsProps {
form: LlmProviderConfig;
onFormChange: (form: LlmProviderConfig) => void;
}

export function LlmProviderGonkaFields({ form, onFormChange }: LlmProviderGonkaFieldsProps): ReactElement {
const endpointsText = serializeEndpoints(form.endpoints);

return (
<Col md={12}>
<div className="border rounded-3 p-3 mt-2 bg-body-tertiary">
<Row className="g-2">
<Col md={6}>
<Form.Group className="mb-2">
<Form.Label className="small fw-medium">Gonka Source URL</Form.Label>
<Form.Control
size="sm"
type="url"
value={form.sourceUrl ?? ''}
onChange={(event) => onFormChange({ ...form, sourceUrl: toNullableString(event.target.value) })}
placeholder="https://node3.gonka.ai"
/>
<Form.Text className="text-body-secondary">
Used to discover active transfer-agent endpoints when no explicit endpoints are set.
</Form.Text>
</Form.Group>
</Col>
<Col md={6}>
<Form.Group className="mb-2">
<Form.Label className="small fw-medium">Requester Address</Form.Label>
<Form.Control
size="sm"
value={form.gonkaAddress ?? ''}
onChange={(event) => onFormChange({ ...form, gonkaAddress: toNullableString(event.target.value) })}
placeholder="Optional; derived from private key when empty"
/>
</Form.Group>
</Col>
<Col md={12}>
<Form.Group className="mb-2">
<Form.Label className="small fw-medium">Explicit Endpoints</Form.Label>
<Form.Control
as="textarea"
rows={2}
size="sm"
value={endpointsText}
onChange={(event) => onFormChange({ ...form, endpoints: parseEndpoints(event.target.value) })}
placeholder="https://host/v1;gonka1transferaddress"
/>
<Form.Text className="text-body-secondary">
Optional comma/newline-separated pairs in url;transferAddress format. Overrides Source URL discovery.
</Form.Text>
</Form.Group>
</Col>
<Col md={12}>
<Button
type="button"
variant="secondary"
size="sm"
onClick={() => onFormChange({ ...form, sourceUrl: 'https://node3.gonka.ai' })}
>
Use public node
</Button>
</Col>
</Row>
</div>
</Col>
);
}

function serializeEndpoints(endpoints: LlmProviderConfig['endpoints']): string {
return (endpoints ?? [])
.map((endpoint) => `${endpoint.url};${endpoint.transferAddress}`)
.join('\n');
}

function parseEndpoints(value: string): LlmProviderConfig['endpoints'] {
return value
.split(/[\n,]/)
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0)
.flatMap((entry) => {
const parts = entry.split(';');
if (parts.length !== 2) {
return [];
}
const url = parts[0].trim();
const transferAddress = parts[1].trim();
return url.length > 0 && transferAddress.length > 0 ? [{ url, transferAddress }] : [];
});
}
3 changes: 3 additions & 0 deletions dashboard/src/pages/settings/LlmProvidersTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ const config: LlmConfig = {
requestTimeoutSeconds: 30,
apiType: 'openai',
legacyApi: null,
sourceUrl: null,
gonkaAddress: null,
endpoints: [],
},
},
};
Expand Down
10 changes: 9 additions & 1 deletion dashboard/src/pages/settings/LlmProvidersTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,15 @@ export default function LlmProvidersTab({ config, modelRouter }: LlmProvidersTab
return;
}
setEditingName(name);
setEditForm({ ...provider, apiKey: null, apiType: normalizeApiType(provider.apiType), legacyApi: provider.legacyApi ?? null });
setEditForm({
...provider,
apiKey: null,
apiType: normalizeApiType(provider.apiType),
legacyApi: provider.legacyApi ?? null,
sourceUrl: provider.sourceUrl ?? null,
gonkaAddress: provider.gonkaAddress ?? null,
endpoints: provider.endpoints ?? [],
});
setIsNewProvider(false);
setShowKey(false);
};
Expand Down
6 changes: 6 additions & 0 deletions dashboard/src/pages/settings/ModelCatalogTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const llmConfig: LlmConfig = {
requestTimeoutSeconds: null,
apiType: 'openai',
legacyApi: null,
sourceUrl: null,
gonkaAddress: null,
endpoints: [],
},
openrouter: {
apiKey: null,
Expand All @@ -28,6 +31,9 @@ const llmConfig: LlmConfig = {
requestTimeoutSeconds: 30,
apiType: 'openai',
legacyApi: null,
sourceUrl: null,
gonkaAddress: null,
endpoints: [],
},
},
};
Expand Down
9 changes: 9 additions & 0 deletions dashboard/src/pages/settings/ModelsTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ const llmConfig: LlmConfig = {
requestTimeoutSeconds: null,
apiType: 'openai',
legacyApi: null,
sourceUrl: null,
gonkaAddress: null,
endpoints: [],
},
anthropic: {
apiKey: null,
Expand All @@ -54,6 +57,9 @@ const llmConfig: LlmConfig = {
requestTimeoutSeconds: null,
apiType: 'anthropic',
legacyApi: null,
sourceUrl: null,
gonkaAddress: null,
endpoints: [],
},
openrouter: {
apiKey: null,
Expand All @@ -62,6 +68,9 @@ const llmConfig: LlmConfig = {
requestTimeoutSeconds: 30,
apiType: 'openai',
legacyApi: null,
sourceUrl: null,
gonkaAddress: null,
endpoints: [],
},
},
};
Expand Down
15 changes: 13 additions & 2 deletions dashboard/src/pages/settings/llmProvidersSupport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const KNOWN_BASE_URLS: Record<string, string> = {
qwen: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
cerebras: 'https://api.cerebras.ai/v1',
deepinfra: 'https://api.deepinfra.com/v1/openai',
gonka: 'https://node3.gonka.ai/v1',
};

export const KNOWN_PROVIDERS: string[] = Object.keys(KNOWN_BASE_URLS);
Expand All @@ -32,9 +33,10 @@ export const PROVIDER_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
const KNOWN_API_TYPES: Record<string, ApiType> = {
anthropic: 'anthropic',
google: 'gemini',
gonka: 'gonka',
};

export const API_TYPE_OPTIONS: ApiType[] = ['openai', 'anthropic', 'gemini'];
export const API_TYPE_OPTIONS: ApiType[] = ['openai', 'anthropic', 'gemini', 'gonka'];

export const API_TYPE_DETAILS: Record<ApiType, ApiTypeDetail> = {
openai: {
Expand All @@ -55,6 +57,12 @@ export const API_TYPE_DETAILS: Record<ApiType, ApiTypeDetail> = {
badgeBg: 'success-subtle',
badgeText: 'success',
},
gonka: {
label: 'Gonka',
help: 'Gonka protocol: OpenAI chat completions with ECDSA request signing.',
badgeBg: 'dark',
badgeText: 'light',
},
};

export function toNullableString(value: string): string | null {
Expand Down Expand Up @@ -82,7 +90,7 @@ export function getDefaultApiTypeForProvider(name: string): ApiType {
}

export function getSuggestedBaseUrl(name: string, apiType: ApiType): string | null {
if (apiType === 'gemini') {
if (apiType === 'gemini' || apiType === 'gonka') {
return null;
}

Expand All @@ -103,5 +111,8 @@ export function buildDefaultProviderConfig(name: string): LlmProviderConfig {
requestTimeoutSeconds: 300,
apiType: defaultApiType,
legacyApi: null,
sourceUrl: defaultApiType === 'gonka' ? 'https://node3.gonka.ai' : null,
gonkaAddress: null,
endpoints: [],
};
}
Loading
Loading