Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
237d1e2060 | ||
|
|
aa498f4cbb | ||
|
|
8f38fd53e4 | ||
|
|
c612120b7e | ||
|
|
e874ce9c4c |
+4
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
TurkServis'in Hermes Agent tabanlı masaüstü geliştirme ortamı.
|
TurkServis'in Hermes Agent tabanlı masaüstü geliştirme ortamı.
|
||||||
|
|
||||||
- Ürün sürümü: **1.0.0-beta.1**, ilk paket hedefi Windows x64.
|
- Ürün sürümü: **1.0.0-beta.4**, ilk paket hedefi Windows x64.
|
||||||
- Varsayılan dil Türkçe; ayarlardan diğer dillere geçilebilir.
|
- Varsayılan dil Türkçe; ayarlardan diğer dillere geçilebilir.
|
||||||
- İlk kurulum, sohbet, dosyalar, terminal ve temel ayarlar Türkçeleştirilmiştir.
|
- İlk kurulum, sohbet, dosyalar, terminal ve temel ayarlar Türkçeleştirilmiştir.
|
||||||
Henüz çevrilmeyen gelişmiş ekranlar ve üçüncü taraf eklentiler İngilizceye döner.
|
Henüz çevrilmeyen gelişmiş ekranlar ve üçüncü taraf eklentiler İngilizceye döner.
|
||||||
@@ -10,6 +10,9 @@ TurkServis'in Hermes Agent tabanlı masaüstü geliştirme ortamı.
|
|||||||
- TurkServis bağlantısı `https://ai.turkservis.online/v1` üzerinden kullanıcının
|
- TurkServis bağlantısı `https://ai.turkservis.online/v1` üzerinden kullanıcının
|
||||||
kendi API anahtarını doğrular ve hesabına açık modelleri dinamik olarak getirir.
|
kendi API anahtarını doğrular ve hesabına açık modelleri dinamik olarak getirir.
|
||||||
Kaynakta veya kurulum paketinde müşteri API anahtarı bulunmaz.
|
Kaynakta veya kurulum paketinde müşteri API anahtarı bulunmaz.
|
||||||
|
- AITURK model kataloğu arka planda otomatik yenilenir. Açık model listeleri
|
||||||
|
dakikada bir sunucuyu kontrol eder; 9Router ve OmniRoute'ta yeni yayımlanan
|
||||||
|
kayıtlar elle liste düzenlemeden görünür. Admin engeli sunucuda uygulanır.
|
||||||
- Bu bağlantı model sağlayıcısıdır; yerel ajan ve dosya araçları bilgisayarda
|
- Bu bağlantı model sağlayıcısıdır; yerel ajan ve dosya araçları bilgisayarda
|
||||||
çalışır. Uzak Hermes gateway bağlantısı farklı bir özelliktir.
|
çalışır. Uzak Hermes gateway bağlantısı farklı bir özelliktir.
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,23 @@ import assert from 'node:assert/strict'
|
|||||||
|
|
||||||
import { test } from 'vitest'
|
import { test } from 'vitest'
|
||||||
|
|
||||||
import { classifyActiveRuntime, hasValidBootstrapMarker } from './active-runtime-state'
|
import { classifyActiveRuntime, hasValidBootstrapMarker, needsPackagedRuntimeUpgrade } from './active-runtime-state'
|
||||||
|
|
||||||
const VALID_MARKER = {
|
const VALID_MARKER = {
|
||||||
pinnedCommit: '1234567890abcdef1234567890abcdef12345678',
|
pinnedCommit: '1234567890abcdef1234567890abcdef12345678',
|
||||||
schemaVersion: 1
|
schemaVersion: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test('a package upgrade refreshes only a proven managed runtime, once per package', () => {
|
||||||
|
const stamp = {commit: 'b'.repeat(40), source: 'git'}
|
||||||
|
assert.equal(needsPackagedRuntimeUpgrade(true, stamp, VALID_MARKER), true)
|
||||||
|
assert.equal(needsPackagedRuntimeUpgrade(true, stamp, {...VALID_MARKER, pinnedCommit: stamp.commit}), false)
|
||||||
|
assert.equal(needsPackagedRuntimeUpgrade(true, stamp, {...VALID_MARKER, pinnedCommit: 'c'.repeat(40), packageCommit: stamp.commit}), false)
|
||||||
|
assert.equal(needsPackagedRuntimeUpgrade(false, stamp, VALID_MARKER), false)
|
||||||
|
assert.equal(needsPackagedRuntimeUpgrade(true, stamp, null), false)
|
||||||
|
assert.equal(needsPackagedRuntimeUpgrade(true, {commit: '0'.repeat(40), source: 'fallback'}, VALID_MARKER), false)
|
||||||
|
})
|
||||||
|
|
||||||
test('hasValidBootstrapMarker accepts the current schema with a real-looking commit', () => {
|
test('hasValidBootstrapMarker accepts the current schema with a real-looking commit', () => {
|
||||||
assert.equal(hasValidBootstrapMarker(VALID_MARKER, 1), true)
|
assert.equal(hasValidBootstrapMarker(VALID_MARKER, 1), true)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
export interface BootstrapMarkerLike {
|
export interface BootstrapMarkerLike {
|
||||||
pinnedCommit?: unknown
|
pinnedCommit?: unknown
|
||||||
|
packageCommit?: unknown
|
||||||
schemaVersion?: unknown
|
schemaVersion?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An AITURK package upgrade must bring its managed agent along with the UI.
|
||||||
|
// Developer checkouts and installs without proven Desktop ownership keep their
|
||||||
|
// existing launch behavior. The installer preserves edits and refuses rollback.
|
||||||
|
export function needsPackagedRuntimeUpgrade(
|
||||||
|
packaged: boolean, stamp: { commit?: string; source?: string } | null,
|
||||||
|
marker: BootstrapMarkerLike | null
|
||||||
|
): boolean {
|
||||||
|
return Boolean(packaged && stamp?.source !== 'fallback' &&
|
||||||
|
/^[0-9a-f]{40}$/i.test(stamp?.commit || '') && !/^0+$/.test(stamp?.commit || '') &&
|
||||||
|
hasValidBootstrapMarker(marker, 1) && (marker?.packageCommit || marker?.pinnedCommit) !== stamp?.commit)
|
||||||
|
}
|
||||||
|
|
||||||
export interface ActiveRuntimeState {
|
export interface ActiveRuntimeState {
|
||||||
hasValidMarker: boolean
|
hasValidMarker: boolean
|
||||||
shouldUseActiveRuntime: boolean
|
shouldUseActiveRuntime: boolean
|
||||||
|
|||||||
@@ -143,8 +143,8 @@ test('resolveMarkerPinnedCommit prefers real HEAD over fallback stamp zeros', ()
|
|||||||
resolveMarkerPinnedCommit({ commit: 'd'.repeat(40), branch: 'main' }, '/tmp/checkout', {
|
resolveMarkerPinnedCommit({ commit: 'd'.repeat(40), branch: 'main' }, '/tmp/checkout', {
|
||||||
resolveHead: () => realHead
|
resolveHead: () => realHead
|
||||||
}),
|
}),
|
||||||
'd'.repeat(40),
|
realHead,
|
||||||
'packaged real pin wins over checkout HEAD'
|
'marker reports the actual installed checkout, including a preserved newer runtime'
|
||||||
)
|
)
|
||||||
assert.equal(
|
assert.equal(
|
||||||
resolveMarkerPinnedCommit({ commit: ZERO_COMMIT, branch: 'main' }, '/tmp/missing', {
|
resolveMarkerPinnedCommit({ commit: ZERO_COMMIT, branch: 'main' }, '/tmp/missing', {
|
||||||
|
|||||||
@@ -111,16 +111,16 @@ function resolveMarkerPinnedCommit(
|
|||||||
): string | null {
|
): string | null {
|
||||||
const resolveHead = opts.resolveHead || resolveCheckoutHead
|
const resolveHead = opts.resolveHead || resolveCheckoutHead
|
||||||
|
|
||||||
if (installStamp && isPinnedCommit(installStamp.commit)) {
|
|
||||||
return installStamp.commit
|
|
||||||
}
|
|
||||||
|
|
||||||
const head = resolveHead(activeRoot)
|
const head = resolveHead(activeRoot)
|
||||||
|
|
||||||
if (head) {
|
if (head) {
|
||||||
return head
|
return head
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (installStamp && isPinnedCommit(installStamp.commit)) {
|
||||||
|
return installStamp.commit
|
||||||
|
}
|
||||||
|
|
||||||
return readExistingPinnedCommit(activeRoot)
|
return readExistingPinnedCommit(activeRoot)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -918,14 +918,14 @@ async function runBootstrap(opts) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const existingCheckout = hasExistingGitCheckout(activeRoot)
|
const existingCheckout = hasExistingGitCheckout(activeRoot)
|
||||||
const pinCommit = !existingCheckout
|
const pinCommit = Boolean(installStamp && isPinnedCommit(installStamp.commit)) || !existingCheckout
|
||||||
|
|
||||||
if (existingCheckout && installStamp && installStamp.commit) {
|
if (existingCheckout && installStamp && installStamp.commit) {
|
||||||
emit({
|
emit({
|
||||||
type: 'log',
|
type: 'log',
|
||||||
line:
|
line:
|
||||||
`[bootstrap] existing checkout detected at ${activeRoot}; ` +
|
`[bootstrap] existing checkout detected at ${activeRoot}; ` +
|
||||||
`not pinning to packaged install stamp ${installStamp.commit.slice(0, 12)}`
|
`applying packaged install stamp ${installStamp.commit.slice(0, 12)} with installer rollback protection`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1002,6 +1002,7 @@ async function runBootstrap(opts) {
|
|||||||
|
|
||||||
const markerPayload = {
|
const markerPayload = {
|
||||||
pinnedCommit,
|
pinnedCommit,
|
||||||
|
packageCommit: installStamp && isPinnedCommit(installStamp.commit) ? installStamp.commit : null,
|
||||||
pinnedBranch: installStamp ? installStamp.branch : null
|
pinnedBranch: installStamp ? installStamp.branch : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
systemPreferences
|
systemPreferences
|
||||||
} from 'electron'
|
} from 'electron'
|
||||||
|
|
||||||
import { classifyActiveRuntime } from './active-runtime-state'
|
import { classifyActiveRuntime, needsPackagedRuntimeUpgrade } from './active-runtime-state'
|
||||||
import { destroyKeepaliveAgents, downloadAgentFor, jsonAgentFor, withRetry } from './api-transport'
|
import { destroyKeepaliveAgents, downloadAgentFor, jsonAgentFor, withRetry } from './api-transport'
|
||||||
import { appIconCandidates, resolveAppIcon } from './app-icon'
|
import { appIconCandidates, resolveAppIcon } from './app-icon'
|
||||||
import { stopBackendChild as stopBackendChildImpl, stopBackendTreesForUpdate } from './backend-child'
|
import { stopBackendChild as stopBackendChildImpl, stopBackendTreesForUpdate } from './backend-child'
|
||||||
@@ -4597,6 +4597,7 @@ function writeBootstrapMarker(payload) {
|
|||||||
const merged = {
|
const merged = {
|
||||||
schemaVersion: BOOTSTRAP_MARKER_SCHEMA_VERSION,
|
schemaVersion: BOOTSTRAP_MARKER_SCHEMA_VERSION,
|
||||||
pinnedCommit: payload.pinnedCommit || null,
|
pinnedCommit: payload.pinnedCommit || null,
|
||||||
|
packageCommit: payload.packageCommit || null,
|
||||||
pinnedBranch: payload.pinnedBranch || null,
|
pinnedBranch: payload.pinnedBranch || null,
|
||||||
completedAt: new Date().toISOString(),
|
completedAt: new Date().toISOString(),
|
||||||
desktopVersion: app.getVersion()
|
desktopVersion: app.getVersion()
|
||||||
@@ -4905,6 +4906,11 @@ function resolveHermesBackend(backendArgs) {
|
|||||||
// bootstrap when the runtime itself is unusable.
|
// bootstrap when the runtime itself is unusable.
|
||||||
const activeRuntime = activeRuntimeState()
|
const activeRuntime = activeRuntimeState()
|
||||||
|
|
||||||
|
if (needsPackagedRuntimeUpgrade(IS_PACKAGED, INSTALL_STAMP, readBootstrapMarker())) {
|
||||||
|
rememberLog('[bootstrap] AITURK package changed; updating its managed agent before launch.')
|
||||||
|
return createBootstrapBackend(backendArgs)
|
||||||
|
}
|
||||||
|
|
||||||
if (activeRuntime.shouldUseActiveRuntime && !bootstrapRepairRequested) {
|
if (activeRuntime.shouldUseActiveRuntime && !bootstrapRepairRequested) {
|
||||||
if (!activeRuntime.hasValidMarker) {
|
if (!activeRuntime.hasValidMarker) {
|
||||||
rememberLog(
|
rememberLog(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "aiturk-ide",
|
"name": "aiturk-ide",
|
||||||
"productName": "AITURK IDE",
|
"productName": "AITURK IDE",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0-beta.1",
|
"version": "1.0.0-beta.4",
|
||||||
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
|
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
|
||||||
"author": "AITURK / TurkServis",
|
"author": "AITURK / TurkServis",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ describe('the catalog owns model curation', () => {
|
|||||||
renderMenu()
|
renderMenu()
|
||||||
await screen.findByText(/Gemini 2\.5 Flash/i)
|
await screen.findByText(/Gemini 2\.5 Flash/i)
|
||||||
|
|
||||||
const input = screen.getByRole('textbox', { name: 'Search models' })
|
const input = screen.getByRole('textbox', { name: /Search models|Model ara/ })
|
||||||
|
|
||||||
fireEvent.change(input, { target: { value: 'gemini-3.1' } })
|
fireEvent.change(input, { target: { value: 'gemini-3.1' } })
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ describe('the catalog owns model curation', () => {
|
|||||||
renderMenu()
|
renderMenu()
|
||||||
await screen.findByText(/Gemini 3\.1 Pro/i)
|
await screen.findByText(/Gemini 3\.1 Pro/i)
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('Edit models…'))
|
fireEvent.click(screen.getByText(/Edit models…|Modelleri düzenle…/))
|
||||||
|
|
||||||
expect($modelVisibilityOpen.get()).toBe(true)
|
expect($modelVisibilityOpen.get()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function aiturkInstallStage(name: string): string | undefined {
|
|||||||
|
|
||||||
const en = {
|
const en = {
|
||||||
intro: 'Bring your code, question, or idea. Build with AITURK.',
|
intro: 'Bring your code, question, or idea. Build with AITURK.',
|
||||||
pitch: 'Connect your models with your TurkServis account.',
|
pitch: 'Connect your models and available image and audio tools with your TurkServis account.',
|
||||||
key: 'TurkServis API key',
|
key: 'TurkServis API key',
|
||||||
getKey: 'My account and API keys',
|
getKey: 'My account and API keys',
|
||||||
noModels: 'No models are available for this key.',
|
noModels: 'No models are available for this key.',
|
||||||
@@ -74,7 +74,7 @@ const en = {
|
|||||||
|
|
||||||
const tr: typeof en = {
|
const tr: typeof en = {
|
||||||
intro: 'Kodunuzu, sorunuzu veya fikrinizi paylaşın. AITURK ile birlikte geliştirin.',
|
intro: 'Kodunuzu, sorunuzu veya fikrinizi paylaşın. AITURK ile birlikte geliştirin.',
|
||||||
pitch: 'TurkServis hesabınızla modellerinize bağlanın.',
|
pitch: 'TurkServis hesabınızla modellerinize, kullanılabilir görsel ve ses araçlarına bağlanın.',
|
||||||
key: 'TurkServis API anahtarı',
|
key: 'TurkServis API anahtarı',
|
||||||
getKey: 'Hesabım ve API anahtarlarım',
|
getKey: 'Hesabım ve API anahtarlarım',
|
||||||
noModels: 'Bu anahtarla kullanılabilecek model bulunamadı.',
|
noModels: 'Bu anahtarla kullanılabilecek model bulunamadı.',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { beforeEach, describe, expect, it } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { QueryObserver, focusManager } from '@tanstack/react-query'
|
||||||
|
|
||||||
import { invalidateProfileScopedQueries, queryClient } from './query-client'
|
import { invalidateProfileScopedQueries, queryClient } from './query-client'
|
||||||
|
|
||||||
@@ -11,6 +12,29 @@ describe('invalidateProfileScopedQueries', () => {
|
|||||||
queryClient.clear()
|
queryClient.clear()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('refreshes an observed model list without reopening the picker', async () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
focusManager.setFocused(true)
|
||||||
|
let models = ['old-model']
|
||||||
|
const observer = new QueryObserver(queryClient, {
|
||||||
|
queryKey: ['model-options', 'selected-profile'],
|
||||||
|
queryFn: async () => models
|
||||||
|
})
|
||||||
|
const unsubscribe = observer.subscribe(() => {})
|
||||||
|
try {
|
||||||
|
await observer.refetch()
|
||||||
|
expect(observer.getCurrentResult().data).toEqual(['old-model'])
|
||||||
|
models = ['new-model', 'new-combo']
|
||||||
|
await vi.advanceTimersByTimeAsync(60_000)
|
||||||
|
expect(observer.getCurrentResult().data).toEqual(models)
|
||||||
|
} finally {
|
||||||
|
unsubscribe()
|
||||||
|
queryClient.clear()
|
||||||
|
focusManager.setFocused(undefined)
|
||||||
|
vi.useRealTimers()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('invalidates profile-scoped caches and leaves account/global caches intact', () => {
|
it('invalidates profile-scoped caches and leaves account/global caches intact', () => {
|
||||||
const profileScoped = [
|
const profileScoped = [
|
||||||
['hermes-config-record'],
|
['hermes-config-record'],
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ export const queryClient = new QueryClient({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
queryClient.setQueryDefaults(['model-options'], {
|
||||||
|
staleTime: 30_000,
|
||||||
|
refetchInterval: 60_000,
|
||||||
|
refetchOnWindowFocus: true
|
||||||
|
})
|
||||||
|
|
||||||
// Curried, setState-shaped cache writer for optimistic write-through: keeps
|
// Curried, setState-shaped cache writer for optimistic write-through: keeps
|
||||||
// mutation sites terse (`setX(next)` or `setX(prev => …)`) over one query key.
|
// mutation sites terse (`setX(next)` or `setX(prev => …)`) over one query key.
|
||||||
export const writeCache =
|
export const writeCache =
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""Connect the AITURK distribution's media tools to its existing member key."""
|
||||||
|
|
||||||
|
|
||||||
|
def configure_existing_media(cfg: dict) -> bool:
|
||||||
|
"""Migrate an existing member endpoint when the upgraded backend starts."""
|
||||||
|
providers = cfg.get("providers") or {}
|
||||||
|
entries = providers.values() if isinstance(providers, dict) else []
|
||||||
|
for entry in entries:
|
||||||
|
if isinstance(entry, dict) and configure_media(cfg, entry):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def configure_media(cfg: dict, entry: dict) -> bool:
|
||||||
|
if entry.get("base_url", "").rstrip("/") != "https://ai.turkservis.online/v1":
|
||||||
|
return False
|
||||||
|
key_env = entry.get("key_env")
|
||||||
|
if not isinstance(key_env, str) or not key_env:
|
||||||
|
return False
|
||||||
|
servers = cfg.get("mcp_servers")
|
||||||
|
if servers is None:
|
||||||
|
servers = cfg["mcp_servers"] = {}
|
||||||
|
if not isinstance(servers, dict):
|
||||||
|
return False
|
||||||
|
name = "turkservis-media"
|
||||||
|
existing = servers.get(name, {})
|
||||||
|
if not isinstance(existing, dict):
|
||||||
|
return False
|
||||||
|
if existing and existing.get("url") != "https://ai.turkservis.online/mcp":
|
||||||
|
return False
|
||||||
|
updated = {
|
||||||
|
**existing,
|
||||||
|
"url": "https://ai.turkservis.online/mcp",
|
||||||
|
"headers": {"Authorization": "Bearer ${" + key_env + "}"},
|
||||||
|
"timeout": 300,
|
||||||
|
"tools": {"include": ["list_capabilities", "generate_media"]},
|
||||||
|
}
|
||||||
|
if updated == existing:
|
||||||
|
return False
|
||||||
|
servers[name] = updated
|
||||||
|
return True
|
||||||
+17
-4
@@ -8,6 +8,7 @@ Add, remove, or reorder entries here — both `hermes setup` and
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
import contextvars
|
||||||
import json
|
import json
|
||||||
import http.client
|
import http.client
|
||||||
import logging
|
import logging
|
||||||
@@ -4809,10 +4810,11 @@ def _spawn_swr_refresh(cache_key: str, refresh_fn=None) -> None:
|
|||||||
``PROVIDER_REGISTRY`` slug and refreshed via :func:`provider_model_ids`
|
``PROVIDER_REGISTRY`` slug and refreshed via :func:`provider_model_ids`
|
||||||
(the original behavior).
|
(the original behavior).
|
||||||
"""
|
"""
|
||||||
|
refresh_identity = (str(_provider_models_cache_path()), cache_key)
|
||||||
with _swr_refresh_lock:
|
with _swr_refresh_lock:
|
||||||
if cache_key in _swr_refresh_inflight:
|
if refresh_identity in _swr_refresh_inflight:
|
||||||
return
|
return
|
||||||
_swr_refresh_inflight.add(cache_key)
|
_swr_refresh_inflight.add(refresh_identity)
|
||||||
|
|
||||||
def _default_refresh():
|
def _default_refresh():
|
||||||
live = provider_model_ids(cache_key, force_refresh=True)
|
live = provider_model_ids(cache_key, force_refresh=True)
|
||||||
@@ -4847,10 +4849,10 @@ def _spawn_swr_refresh(cache_key: str, refresh_fn=None) -> None:
|
|||||||
logger.debug("SWR refresh failed for %s", cache_key, exc_info=True)
|
logger.debug("SWR refresh failed for %s", cache_key, exc_info=True)
|
||||||
finally:
|
finally:
|
||||||
with _swr_refresh_lock:
|
with _swr_refresh_lock:
|
||||||
_swr_refresh_inflight.discard(cache_key)
|
_swr_refresh_inflight.discard(refresh_identity)
|
||||||
|
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=_refresh, daemon=True, name=f"model-cache-swr-{cache_key}"
|
target=contextvars.copy_context().run, args=(_refresh,), daemon=True, name=f"model-cache-swr-{cache_key}"
|
||||||
).start()
|
).start()
|
||||||
|
|
||||||
|
|
||||||
@@ -6855,6 +6857,17 @@ def cached_fetch_api_models(
|
|||||||
entry = cache.get(cache_key)
|
entry = cache.get(cache_key)
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
|
if normalized_url == "https://ai.turkservis.online/v1":
|
||||||
|
ttl_seconds = min(ttl_seconds, 30)
|
||||||
|
if cache_only and (not _cache_entry_valid(entry, fp) or now - entry["at"] >= ttl_seconds):
|
||||||
|
def _refresh_aiturk():
|
||||||
|
live = fetch_api_models(api_key, base_url, timeout=timeout,
|
||||||
|
api_mode=api_mode, headers=headers)
|
||||||
|
if live is None:
|
||||||
|
return None
|
||||||
|
return {"fp": fp, "at": time.time(), "models": list(live)}
|
||||||
|
_spawn_swr_refresh(cache_key, _refresh_aiturk)
|
||||||
|
|
||||||
if cache_only:
|
if cache_only:
|
||||||
# Same trust window as the stale-while-revalidate tier below, minus
|
# Same trust window as the stale-while-revalidate tier below, minus
|
||||||
# the revalidation: an entry this side of the bound is good enough to
|
# the revalidation: an entry this side of the bound is good enough to
|
||||||
|
|||||||
@@ -405,6 +405,12 @@ def _eager_reconcile_own_session_db() -> None:
|
|||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def _lifespan(app: "FastAPI"):
|
async def _lifespan(app: "FastAPI"):
|
||||||
|
from hermes_cli.aiturk_media import configure_existing_media
|
||||||
|
from hermes_cli.config import read_raw_config, save_config
|
||||||
|
with _CONFIG_MUTATION_LOCK:
|
||||||
|
media_cfg = read_raw_config()
|
||||||
|
if configure_existing_media(media_cfg):
|
||||||
|
save_config(media_cfg)
|
||||||
app.state.event_channels = {} # dict[str, set]
|
app.state.event_channels = {} # dict[str, set]
|
||||||
app.state.event_lock = asyncio.Lock()
|
app.state.event_lock = asyncio.Lock()
|
||||||
app.state.pty_active_session_files = {} # dict[str, Path]
|
app.state.pty_active_session_files = {} # dict[str, Path]
|
||||||
@@ -8823,6 +8829,9 @@ def _write_custom_endpoint(cfg: Dict[str, Any], body: CustomEndpointUpdate) -> T
|
|||||||
providers[endpoint_id] = entry
|
providers[endpoint_id] = entry
|
||||||
cfg["providers"] = providers
|
cfg["providers"] = providers
|
||||||
|
|
||||||
|
from hermes_cli.aiturk_media import configure_media
|
||||||
|
configure_media(cfg, entry)
|
||||||
|
|
||||||
if body.make_default:
|
if body.make_default:
|
||||||
cfg["model"] = _apply_main_model_assignment(
|
cfg["model"] = _apply_main_model_assignment(
|
||||||
cfg.get("model", {}), endpoint_id, model, base_url
|
cfg.get("model", {}), endpoint_id, model, base_url
|
||||||
|
|||||||
Generated
+7
-7
@@ -65,7 +65,8 @@
|
|||||||
},
|
},
|
||||||
"apps/desktop": {
|
"apps/desktop": {
|
||||||
"name": "aiturk-ide",
|
"name": "aiturk-ide",
|
||||||
"version": "1.0.0-beta.1",
|
"version": "1.0.0-beta.4",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@assistant-ui/core": "0.2.23",
|
"@assistant-ui/core": "0.2.23",
|
||||||
"@assistant-ui/react": "0.14.24",
|
"@assistant-ui/react": "0.14.24",
|
||||||
@@ -175,8 +176,7 @@
|
|||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"get-windows": "9.3.0"
|
"get-windows": "9.3.0"
|
||||||
},
|
}
|
||||||
"license": "MIT"
|
|
||||||
},
|
},
|
||||||
"apps/desktop/node_modules/@babel/code-frame": {
|
"apps/desktop/node_modules/@babel/code-frame": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
@@ -7465,6 +7465,10 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/aiturk-ide": {
|
||||||
|
"resolved": "apps/desktop",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.15.0",
|
"version": "6.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
|
||||||
@@ -19873,10 +19877,6 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"node_modules/aiturk-ide": {
|
|
||||||
"resolved": "apps/desktop",
|
|
||||||
"link": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
"""An open AITURK picker discovers changes without an explicit refresh."""
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
from hermes_cli import models
|
||||||
|
|
||||||
|
|
||||||
|
def test_cache_only_picker_refreshes_stale_aiturk_catalog(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||||
|
url = "https://ai.turkservis.online/v1"
|
||||||
|
key = "test-only"
|
||||||
|
fp = models._custom_endpoint_fingerprint(key, None, None)
|
||||||
|
models._save_provider_models_cache({"custom:" + url: {
|
||||||
|
"fp": fp, "at": time.time() - 90, "models": ["old"]}})
|
||||||
|
started = threading.Event()
|
||||||
|
release = threading.Event()
|
||||||
|
|
||||||
|
def fetch(*args, **kwargs):
|
||||||
|
started.set()
|
||||||
|
assert release.wait(5)
|
||||||
|
return ["new-model", "new-combo"]
|
||||||
|
|
||||||
|
monkeypatch.setattr(models, "fetch_api_models", fetch)
|
||||||
|
try:
|
||||||
|
assert models.cached_fetch_api_models(key, url, cache_only=True) == ["old"]
|
||||||
|
assert started.wait(5)
|
||||||
|
finally:
|
||||||
|
release.set()
|
||||||
|
deadline = time.monotonic() + 5
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
result = models.cached_fetch_api_models(key, url, cache_only=True)
|
||||||
|
if result == ["new-model", "new-combo"]:
|
||||||
|
break
|
||||||
|
time.sleep(0.02)
|
||||||
|
assert result == ["new-model", "new-combo"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_background_refresh_preserves_context_scope(tmp_path, monkeypatch):
|
||||||
|
import contextvars
|
||||||
|
scope = contextvars.ContextVar("test_catalog_scope", default="wrong")
|
||||||
|
seen = []
|
||||||
|
done = threading.Event()
|
||||||
|
monkeypatch.setattr(models, "_provider_models_cache_path", lambda: tmp_path / scope.get())
|
||||||
|
monkeypatch.setattr(models, "_load_provider_models_cache", lambda: {})
|
||||||
|
def save(cache):
|
||||||
|
seen.append((scope.get(), cache))
|
||||||
|
done.set()
|
||||||
|
monkeypatch.setattr(models, "_save_provider_models_cache", save)
|
||||||
|
token = scope.set("selected-profile")
|
||||||
|
try:
|
||||||
|
models._spawn_swr_refresh("scope-test", lambda: {"models": [scope.get()]})
|
||||||
|
finally:
|
||||||
|
scope.reset(token)
|
||||||
|
assert done.wait(5)
|
||||||
|
assert seen == [("selected-profile", {"scope-test": {"models": ["selected-profile"]}})]
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from hermes_cli.aiturk_media import configure_media, configure_existing_media
|
||||||
|
|
||||||
|
|
||||||
|
def test_existing_member_profile_migrates_once_without_changing_provider():
|
||||||
|
entry = {"base_url": "https://ai.turkservis.online/v1", "key_env": "MEMBER_KEY"}
|
||||||
|
cfg = {"providers": {"turkservis": dict(entry)}, "model": {"default": "kept"}}
|
||||||
|
assert configure_existing_media(cfg)
|
||||||
|
assert cfg["providers"]["turkservis"] == entry
|
||||||
|
assert cfg["model"] == {"default": "kept"}
|
||||||
|
assert not configure_existing_media(cfg)
|
||||||
|
|
||||||
|
|
||||||
|
def test_member_key_is_referenced_without_copying_or_overwriting_other_servers():
|
||||||
|
cfg = {"mcp_servers": {"other": {"url": "https://example.org/mcp"}}}
|
||||||
|
entry = {"base_url": "https://ai.turkservis.online/v1", "key_env": "HERMES_CUSTOM_TURKSERVIS_API_KEY"}
|
||||||
|
assert configure_media(cfg, entry)
|
||||||
|
assert cfg["mcp_servers"]["other"] == {"url": "https://example.org/mcp"}
|
||||||
|
assert cfg["mcp_servers"]["turkservis-media"]["headers"]["Authorization"] == "Bearer ${HERMES_CUSTOM_TURKSERVIS_API_KEY}"
|
||||||
|
assert not configure_media(cfg, entry)
|
||||||
|
entry["key_env"] = "ROTATED_MEMBER_KEY"
|
||||||
|
assert configure_media(cfg, entry)
|
||||||
|
assert "ROTATED_MEMBER_KEY" in cfg["mcp_servers"]["turkservis-media"]["headers"]["Authorization"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_other_provider_and_missing_key_do_not_enable_media():
|
||||||
|
for entry in ({"base_url": "https://example.com/v1", "key_env": "SECRET"}, {"base_url": "https://ai.turkservis.online/v1"}):
|
||||||
|
cfg = {}
|
||||||
|
assert not configure_media(cfg, entry)
|
||||||
|
assert cfg == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicitly_disabled_connection_stays_disabled():
|
||||||
|
cfg = {"mcp_servers": {"turkservis-media": {"url": "https://ai.turkservis.online/mcp", "enabled": False}}}
|
||||||
|
configure_media(cfg, {"base_url": "https://ai.turkservis.online/v1", "key_env": "KEY"})
|
||||||
|
assert cfg["mcp_servers"]["turkservis-media"]["enabled"] is False
|
||||||
Reference in New Issue
Block a user