feat(ai-chat): surface reasoning from openai-compatible providers (z.ai/GLM) (#175) #177

Merged
Ghost merged 2 commits from feat/reasoning-openai-compatible into develop 2026-06-24 23:19:15 +03:00

Связано с #175. Делает reasoning агента видимым — через ЯВНЫЙ выбор провайдера.

Зачем

glm-5.2 (и DeepSeek и пр.) стримят размышления как reasoning_content, но официальный @ai-sdk/openai это поле не мапит → reasoning терялся.

Дизайн (по ревью)

Вместо вывода по baseUrl — явный chatApiStyle: 'openai-compatible' | 'openai' (зеркало sttApiStyle, без миграции — поле в settings.ai.provider JSON):

  • 'openai-compatible' (дефолт) → @ai-sdk/openai-compatible: мапит стримовый reasoning_content в reasoning-парты (z.ai/GLM, DeepSeek, …). includeUsage: true сохраняет стримовый usage (иначе обнуляются счётчики токенов).
  • 'openai' → официальный провайдер (reasoning-модели реального OpenAI), без маппинга стороннего reasoning_content.
  • Дефолт = openai-compatible → существующие воркспейсы видят reasoning без действий админа. Без baseURL → откат на официальный.

Кастомный baseURL может фронтить и реальный OpenAI (Azure/прокси/o1) — поэтому выбор явный, а не по URL.

Файлы

Бэк: ai.types, update DTO, ai-settings.service (resolve/getMasked/update-allowlist), workspace.repo ALLOWED (второй SQL-allowlist, без него поле не персистилось), ai.service-селектор. Фронт: типы + <Select> Protocol + i18n. Скоуп только chat.

Проверка на стенде

default → reasoning + usage; 'openai' → reasoning пропадает; round-trip настройки работает. 4 теста на селектор; server+client tsc + 36 специ зелёные.

Комплементарно к #176 (таймауты, уже влит).

🤖 Generated with Claude Code

Связано с #175. Делает reasoning агента видимым — через ЯВНЫЙ выбор провайдера. ## Зачем glm-5.2 (и DeepSeek и пр.) стримят размышления как `reasoning_content`, но официальный @ai-sdk/openai это поле не мапит → reasoning терялся. ## Дизайн (по ревью) Вместо вывода по baseUrl — явный `chatApiStyle: 'openai-compatible' | 'openai'` (зеркало sttApiStyle, без миграции — поле в settings.ai.provider JSON): - **'openai-compatible'** (дефолт) → @ai-sdk/openai-compatible: мапит стримовый `reasoning_content` в reasoning-парты (z.ai/GLM, DeepSeek, …). `includeUsage: true` сохраняет стримовый usage (иначе обнуляются счётчики токенов). - **'openai'** → официальный провайдер (reasoning-модели реального OpenAI), без маппинга стороннего reasoning_content. - Дефолт = openai-compatible → существующие воркспейсы видят reasoning без действий админа. Без baseURL → откат на официальный. Кастомный baseURL может фронтить и реальный OpenAI (Azure/прокси/o1) — поэтому выбор явный, а не по URL. ## Файлы Бэк: ai.types, update DTO, ai-settings.service (resolve/getMasked/update-allowlist), **workspace.repo ALLOWED** (второй SQL-allowlist, без него поле не персистилось), ai.service-селектор. Фронт: типы + `<Select>` Protocol + i18n. Скоуп только chat. ## Проверка на стенде default → reasoning + usage; 'openai' → reasoning пропадает; round-trip настройки работает. 4 теста на селектор; server+client tsc + 36 специ зелёные. Комплементарно к #176 (таймауты, уже влит). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Ghost added 1 commit 2026-06-24 22:18:49 +03:00
The agent's chain-of-thought was never shown: glm-5.2 (and other openai-
compatible providers — DeepSeek, etc.) stream their thinking as
`reasoning_content` deltas, but the official @ai-sdk/openai provider does NOT map
that field (verified: 0 occurrences in the package), so our
`createOpenAI(...).chat()` silently dropped it. The model "thinks" server-side
and only the final answer streamed — which also made the connection look idle
during a long reasoning phase.

Fix: for the `openai` driver with a CUSTOM baseURL (an openai-compatible
third-party endpoint), build the model with @ai-sdk/openai-compatible instead.
It maps the streamed `reasoning_content` to reasoning parts (confirmed live: the
stream now carries reasoning-start/delta/end), which the client already renders,
and it targets Chat Completions (the portable endpoint these gateways accept on
multi-turn history). Real OpenAI (no baseURL) keeps the official provider.

Verified on the stand against z.ai glm-5.2: reasoning parts now stream; MCP tool
calls (searxng/crawl4ai), the multi-step agent loop, and a normal finish all
still work.

Tests: ai.service.spec asserts the provider switch (custom baseURL ->
openai-compatible; no baseURL -> openai.chat). AI/mcp specs green. server tsc
clean.

Note: complementary to the long-turn timeout fix (#175 / PR fix/ai-stream-undici-
timeout) — they touch the same openai case and compose at merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Owner

Дизайн: явный селектор «тип протокола» вместо эвристики if (baseUrl)

По итогам ревью этого PR: текущий подход выбирает реализацию провайдера по факту наличия baseUrl (createOpenAICompatible при кастомном URL, иначе официальный createOpenAI). Это хрупко — комбинация «драйвер openai + кастомный baseUrl» не означает «всегда сторонний openai-compatible»: за кастомным URL может стоять и реальный OpenAI (Azure/корпоративный прокси/реальные reasoning-модели o1/o3/gpt-5), для которого официальный провайдер делает спец-формирование запроса (max_completion_tokens, роль developer), а openai-compatible — нет.

Решение: убрать вывод по baseUrl и дать админу явно выбирать тип протокола, по образцу уже существующего sttApiStyle: 'multipart' | 'json'.

Модель данных

Новое необязательное поле в settings.ai.provider (это JSON-блоб в workspace.settingsмиграция БД не нужна):

export type ChatApiStyle = 'openai-compatible' | 'openai';
export const CHAT_API_STYLES: ChatApiStyle[] = ['openai-compatible', 'openai'];
// в AiProviderSettings / MaskedAiSettings:
chatApiStyle?: ChatApiStyle;
  • 'openai-compatible'@ai-sdk/openai-compatible (createOpenAICompatible): мапит стримовый reasoning_content в reasoning-парты (z.ai/GLM, DeepSeek, OpenRouter, …) — то, ради чего затевался #175.
  • 'openai' → официальный @ai-sdk/openai createOpenAI(...).chat(): спец-формирование для reasoning-моделей реального OpenAI, без маппинга стороннего reasoning_content.

Дефолт (поле не задано) = 'openai-compatible'. Это сохраняет поведение/цель PR: в этом форке UI всегда конфигурирует openai + baseUrl и ходит в /chat/completions, так что существующие воркспейсы продолжат видеть reasoning без действий админа.

Логика выбора (ai.service.ts, getChatModel, case 'openai')

case 'openai': {
  // Реализацию провайдера выбирает админ ЯВНО (chatApiStyle), а не вывод по baseUrl.
  // Обе ветки бьют в Chat Completions (/chat/completions).
  const apiStyle = chatApiStyle ?? 'openai-compatible';
  // У openai-compatible нет дефолтного эндпоинта — ему нужен baseURL. Если его нет
  // (реальный OpenAI; либо cross-driver override роли, обнуляющий baseUrl) —
  // безопасный откат на официальный провайдер.
  if (apiStyle === 'openai-compatible' && baseUrl) {
    return createOpenAICompatible({
      name: 'openai-compatible',
      apiKey,
      baseURL: baseUrl,
      includeUsage: true, // сохранить стримовый учёт токенов (паритет с официальным)
      fetch: this.aiDiagnosticFetch,
    })(chatModel);
  }
  return createOpenAI({
    apiKey,
    baseURL: baseUrl, // в этом форке baseUrl всегда задан; undefined = реальный OpenAI
    fetch: this.aiDiagnosticFetch,
  }).chat(chatModel);
}

chatApiStyle читается из resolved-конфига рядом с baseUrl/apiKey.

Два замечания из ревью закрываются здесь же:

  • includeUsage: true устраняет тихую регрессию: @ai-sdk/openai-compatible@2.0.37 шлёт stream_options.include_usage только при includeUsage, иначе usage в стриме = undefined → обнуляются metadata.usage/contextTokens, живой счётчик токенов и reasoning-токены. Официальный провайдер всегда слал include_usage: true.
  • Регрессия по reasoning-моделям реального OpenAI больше не «тихая»: админ осознанно выбирает openai для такого эндпоинта.

Затрагиваемые файлы (точечные правки)

Бэкенд:

  • apps/server/src/integrations/ai/ai.types.ts — тип ChatApiStyle, CHAT_API_STYLES, поле в AiProviderSettings и MaskedAiSettings.
  • apps/server/src/integrations/ai/dto/update-ai-settings.dto.ts@IsOptional() @IsIn(CHAT_API_STYLES) chatApiStyle?.
  • apps/server/src/integrations/ai/ai-settings.service.tschatApiStyle в UpdateAiSettingsInput, в объекте resolve(), в getMasked(), и в allowlist полей в update().
  • apps/server/src/integrations/ai/ai.service.ts — логика выше + чтение chatApiStyle из cfg.

Фронтенд:

  • apps/client/src/features/workspace/services/ai-settings-service.ts — тип + поля в IAiSettings/IAiSettingsUpdate.
  • apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx — поле схемы chatApiStyle: z.enum(["openai-compatible","openai"]), дефолт/гидрация (?? "openai-compatible"), payload, и <Select> в chat-секции (после поля Base URL), по образцу Select для sttApiStyle:
<Select
  mt="sm"
  label={t("Protocol")}
  description={t("How chat requests are sent and how reasoning is surfaced")}
  data={[
    { value: "openai-compatible", label: t("OpenAI-compatible (surfaces reasoning)") },
    { value: "openai", label: t("OpenAI (official)") },
  ]}
  allowDeselect={false}
  disabled={isLoading}
  {...form.getInputProps("chatApiStyle")}
/>

Скоуп

Только chat-путь. Embeddings (getEmbeddingModel) не нужны — reasoning они не стримят; STT уже имеет собственный sttApiStyle. Это устраняет и расхождение «chat vs embeddings», отмеченное в ревью (выбор остаётся осознанным, а не неявным).

Тесты (ai.service.spec.ts)

Перевести существующие кейсы с «по baseUrl» на «по chatApiStyle» и добавить:

  • chatApiStyle: 'openai-compatible' (+ baseUrl) → model.provider содержит 'openai-compatible';
  • chatApiStyle: 'openai' (+ baseUrl) → model.provider === 'openai.chat';
  • поле не задано (+ baseUrl) → дефолт openai-compatible;
  • chatApiStyle: 'openai-compatible' без baseUrl → безопасный откат на openai.chat.

На обсуждение

  • Имя/значения поля: предлагаю chatApiStyle со значениями 'openai-compatible' | 'openai' (зеркалит sttApiStyle). Альтернатива — protocol со значениями 'compatible' | 'official'.
  • Нужен ли третий пункт Auto (= нынешнее поведение по baseUrl)? Считаю, что нет — он сохраняет ровно ту хрупкость, от которой уходим.

🤖 Generated with Claude Code

## Дизайн: явный селектор «тип протокола» вместо эвристики `if (baseUrl)` По итогам ревью этого PR: текущий подход выбирает реализацию провайдера по факту наличия `baseUrl` (`createOpenAICompatible` при кастомном URL, иначе официальный `createOpenAI`). Это хрупко — комбинация «драйвер `openai` + кастомный `baseUrl`» не означает «всегда сторонний openai-compatible»: за кастомным URL может стоять и реальный OpenAI (Azure/корпоративный прокси/реальные reasoning-модели o1/o3/gpt-5), для которого официальный провайдер делает спец-формирование запроса (`max_completion_tokens`, роль `developer`), а openai-compatible — нет. Решение: убрать вывод по `baseUrl` и дать админу **явно** выбирать тип протокола, по образцу уже существующего `sttApiStyle: 'multipart' | 'json'`. ### Модель данных Новое необязательное поле в `settings.ai.provider` (это JSON-блоб в `workspace.settings` — **миграция БД не нужна**): ```ts export type ChatApiStyle = 'openai-compatible' | 'openai'; export const CHAT_API_STYLES: ChatApiStyle[] = ['openai-compatible', 'openai']; // в AiProviderSettings / MaskedAiSettings: chatApiStyle?: ChatApiStyle; ``` - `'openai-compatible'` → `@ai-sdk/openai-compatible` (`createOpenAICompatible`): мапит стримовый `reasoning_content` в reasoning-парты (z.ai/GLM, DeepSeek, OpenRouter, …) — то, ради чего затевался #175. - `'openai'` → официальный `@ai-sdk/openai` `createOpenAI(...).chat()`: спец-формирование для reasoning-моделей реального OpenAI, без маппинга стороннего `reasoning_content`. **Дефолт (поле не задано) = `'openai-compatible'`.** Это сохраняет поведение/цель PR: в этом форке UI всегда конфигурирует `openai` + `baseUrl` и ходит в `/chat/completions`, так что существующие воркспейсы продолжат видеть reasoning без действий админа. ### Логика выбора (`ai.service.ts`, `getChatModel`, `case 'openai'`) ```ts case 'openai': { // Реализацию провайдера выбирает админ ЯВНО (chatApiStyle), а не вывод по baseUrl. // Обе ветки бьют в Chat Completions (/chat/completions). const apiStyle = chatApiStyle ?? 'openai-compatible'; // У openai-compatible нет дефолтного эндпоинта — ему нужен baseURL. Если его нет // (реальный OpenAI; либо cross-driver override роли, обнуляющий baseUrl) — // безопасный откат на официальный провайдер. if (apiStyle === 'openai-compatible' && baseUrl) { return createOpenAICompatible({ name: 'openai-compatible', apiKey, baseURL: baseUrl, includeUsage: true, // сохранить стримовый учёт токенов (паритет с официальным) fetch: this.aiDiagnosticFetch, })(chatModel); } return createOpenAI({ apiKey, baseURL: baseUrl, // в этом форке baseUrl всегда задан; undefined = реальный OpenAI fetch: this.aiDiagnosticFetch, }).chat(chatModel); } ``` `chatApiStyle` читается из resolved-конфига рядом с `baseUrl`/`apiKey`. Два замечания из ревью закрываются здесь же: - **`includeUsage: true`** устраняет тихую регрессию: `@ai-sdk/openai-compatible@2.0.37` шлёт `stream_options.include_usage` только при `includeUsage`, иначе usage в стриме = `undefined` → обнуляются `metadata.usage`/`contextTokens`, живой счётчик токенов и reasoning-токены. Официальный провайдер всегда слал `include_usage: true`. - Регрессия по reasoning-моделям реального OpenAI больше не «тихая»: админ осознанно выбирает `openai` для такого эндпоинта. ### Затрагиваемые файлы (точечные правки) Бэкенд: - `apps/server/src/integrations/ai/ai.types.ts` — тип `ChatApiStyle`, `CHAT_API_STYLES`, поле в `AiProviderSettings` и `MaskedAiSettings`. - `apps/server/src/integrations/ai/dto/update-ai-settings.dto.ts` — `@IsOptional() @IsIn(CHAT_API_STYLES) chatApiStyle?`. - `apps/server/src/integrations/ai/ai-settings.service.ts` — `chatApiStyle` в `UpdateAiSettingsInput`, в объекте `resolve()`, в `getMasked()`, и в allowlist полей в `update()`. - `apps/server/src/integrations/ai/ai.service.ts` — логика выше + чтение `chatApiStyle` из cfg. Фронтенд: - `apps/client/src/features/workspace/services/ai-settings-service.ts` — тип + поля в `IAiSettings`/`IAiSettingsUpdate`. - `apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx` — поле схемы `chatApiStyle: z.enum(["openai-compatible","openai"])`, дефолт/гидрация (`?? "openai-compatible"`), payload, и `<Select>` в chat-секции (после поля Base URL), по образцу Select для `sttApiStyle`: ```tsx <Select mt="sm" label={t("Protocol")} description={t("How chat requests are sent and how reasoning is surfaced")} data={[ { value: "openai-compatible", label: t("OpenAI-compatible (surfaces reasoning)") }, { value: "openai", label: t("OpenAI (official)") }, ]} allowDeselect={false} disabled={isLoading} {...form.getInputProps("chatApiStyle")} /> ``` ### Скоуп Только chat-путь. Embeddings (`getEmbeddingModel`) не нужны — reasoning они не стримят; STT уже имеет собственный `sttApiStyle`. Это устраняет и расхождение «chat vs embeddings», отмеченное в ревью (выбор остаётся осознанным, а не неявным). ### Тесты (`ai.service.spec.ts`) Перевести существующие кейсы с «по baseUrl» на «по `chatApiStyle`» и добавить: - `chatApiStyle: 'openai-compatible'` (+ baseUrl) → `model.provider` содержит `'openai-compatible'`; - `chatApiStyle: 'openai'` (+ baseUrl) → `model.provider === 'openai.chat'`; - поле не задано (+ baseUrl) → дефолт `openai-compatible`; - `chatApiStyle: 'openai-compatible'` без baseUrl → безопасный откат на `openai.chat`. ### На обсуждение - Имя/значения поля: предлагаю `chatApiStyle` со значениями `'openai-compatible' | 'openai'` (зеркалит `sttApiStyle`). Альтернатива — `protocol` со значениями `'compatible' | 'official'`. - Нужен ли третий пункт `Auto` (= нынешнее поведение по baseUrl)? Считаю, что нет — он сохраняет ровно ту хрупкость, от которой уходим. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Ghost force-pushed feat/reasoning-openai-compatible from fe1bbbe806 to 59190148db 2026-06-24 22:58:28 +03:00 Compare
Owner

Ревью #177 — что надо сделать

Регрессию со сменой дефолта на openai-compatible (reasoning-модель реального OpenAI за baseUrl) сознательно оставляем как есть — намеренное поведение. Остальное:

Тесты

  • Усилить новые тесты chatApiStyle. Сейчас они проверяют только .provider, а это не ловит проводку: .provider одинаков вне зависимости от того, передан ли includeUsage. Регрессия, дропающая includeUsage: true (ровно тот баг с обнулением счётчика токенов, от которого предостерегает комментарий), пройдёт зелёной. Надо замокать @ai-sdk/openai-compatible и проверить, что фабрика вызвана с { includeUsage: true, baseURL, apiKey, fetch }.
  • (минор, опционально) Добавить тест валидации DTO @IsIn(CHAT_API_STYLES) (отклоняет мусор, принимает оба валидных значения) и round-trip персиста chatApiStyle (update-allowlist → SQL ALLOWEDresolve/getMasked). Не обязательно — соответствует текущей практике (sttApiStyle уехал без таких тестов).

CHANGELOG

  • Добавить запись в [Unreleased] про новую настройку «Protocol» / chatApiStyle и смену дефолта (openai → openai-compatible), с рефом (#177) — изменение admin-видимое, ровно тот класс, что туда пишется.

Архитектура (вариант А)

  • Убрать тихий дрейф двух runtime-allowlist'ов ключей провайдера. Один и тот же список ключей ведётся вручную в двух местах, которые TS не сверяет: key-loop в ai-settings.service.ts и SQL ALLOWED в workspace.repo.ts. Пропуск во втором молча ломает персист (как и было с этим полем — поле валидируется, проходит сервис, отсеивается на SQL-границе, без ошибки).
    • Завести единую типизированную константу в ai.types.ts:
      export const PROVIDER_SETTINGS_KEYS = [
        'driver', 'chatModel', 'chatApiStyle', 'embeddingModel', 'baseUrl',
        'embeddingBaseUrl', 'sttModel', 'sttBaseUrl', 'sttApiStyle', 'sttLanguage',
        'systemPrompt', 'publicShareChatModel', 'publicShareAssistantRoleId',
      ] as const satisfies readonly (keyof AiProviderSettings)[];
      
      satisfies заставит компилятор отвергать опечатку или ключ, которого нет в AiProviderSettings.
    • ai-settings.service.ts: брать key-loop из PROVIDER_SETTINGS_KEYS вместо локального массива.
    • workspace.repo.ts остаётся generic-слоем (не импортит AI-типы) — добавить parity-тест, утверждающий, что его ALLOWED равен PROVIDER_SETTINGS_KEYS, чтобы любое будущее расхождение падало в CI, а не молча в проде.

🤖 Сгенерировано в рамках code-review (Claude Code)

## Ревью #177 — что надо сделать Регрессию со сменой дефолта на `openai-compatible` (reasoning-модель реального OpenAI за `baseUrl`) сознательно оставляем как есть — намеренное поведение. Остальное: ### Тесты - [ ] Усилить новые тесты `chatApiStyle`. Сейчас они проверяют только `.provider`, а это не ловит проводку: `.provider` одинаков вне зависимости от того, передан ли `includeUsage`. Регрессия, дропающая `includeUsage: true` (ровно тот баг с обнулением счётчика токенов, от которого предостерегает комментарий), пройдёт зелёной. Надо замокать `@ai-sdk/openai-compatible` и проверить, что фабрика вызвана с `{ includeUsage: true, baseURL, apiKey, fetch }`. - [ ] (минор, опционально) Добавить тест валидации DTO `@IsIn(CHAT_API_STYLES)` (отклоняет мусор, принимает оба валидных значения) и round-trip персиста `chatApiStyle` (`update`-allowlist → SQL `ALLOWED` → `resolve`/`getMasked`). Не обязательно — соответствует текущей практике (`sttApiStyle` уехал без таких тестов). ### CHANGELOG - [ ] Добавить запись в `[Unreleased]` про новую настройку «Protocol» / `chatApiStyle` и смену дефолта (openai → openai-compatible), с рефом (#177) — изменение admin-видимое, ровно тот класс, что туда пишется. ### Архитектура (вариант А) - [ ] Убрать тихий дрейф двух runtime-allowlist'ов ключей провайдера. Один и тот же список ключей ведётся вручную в двух местах, которые TS не сверяет: key-loop в `ai-settings.service.ts` и SQL `ALLOWED` в `workspace.repo.ts`. Пропуск во втором молча ломает персист (как и было с этим полем — поле валидируется, проходит сервис, отсеивается на SQL-границе, без ошибки). - Завести единую типизированную константу в `ai.types.ts`: ```ts export const PROVIDER_SETTINGS_KEYS = [ 'driver', 'chatModel', 'chatApiStyle', 'embeddingModel', 'baseUrl', 'embeddingBaseUrl', 'sttModel', 'sttBaseUrl', 'sttApiStyle', 'sttLanguage', 'systemPrompt', 'publicShareChatModel', 'publicShareAssistantRoleId', ] as const satisfies readonly (keyof AiProviderSettings)[]; ``` `satisfies` заставит компилятор отвергать опечатку или ключ, которого нет в `AiProviderSettings`. - `ai-settings.service.ts`: брать key-loop из `PROVIDER_SETTINGS_KEYS` вместо локального массива. - `workspace.repo.ts` остаётся generic-слоем (не импортит AI-типы) — добавить parity-тест, утверждающий, что его `ALLOWED` равен `PROVIDER_SETTINGS_KEYS`, чтобы любое будущее расхождение падало в CI, а не молча в проде. --- 🤖 Сгенерировано в рамках code-review (Claude Code)
Ghost added 1 commit 2026-06-24 23:18:45 +03:00
Addresses the second #177 review:

- Architecture (the silent allowlist drift): the writable provider-setting keys
  were maintained by hand in two TS-uncheckable places — the key-loop in
  ai-settings.service and the SQL ALLOWED list in the generic workspace repo (a
  miss there silently dropped a field on persist, exactly what bit chatApiStyle).
  Introduce one typed source of truth PROVIDER_SETTINGS_KEYS in ai.types
  (`satisfies readonly (keyof AiProviderSettings)[]`), have the service consume
  it, and keep the repo's own copy (it can't import AI types) guarded by a parity
  test so any future drift fails in CI.
- Tests:
  - ai.service.include-usage.spec: mocks @ai-sdk/openai-compatible and asserts the
    factory is called with { includeUsage: true, baseURL, apiKey, fetch, name } —
    `.provider` alone could not catch a dropped includeUsage (the token-usage
    zeroing regression); also asserts the 'openai' style does NOT use it.
  - ai-provider-settings-keys.spec: the allowlist parity check + DTO validation
    for chatApiStyle (@IsIn accepts both values, rejects garbage, optional).
- CHANGELOG: [Unreleased] entries for the new "Protocol" / chatApiStyle setting
  and the default provider change (openai -> openai-compatible). (#175, #177)

server + client tsc clean; 42 ai/settings specs green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ghost merged commit d1fbcc1bfa into develop 2026-06-24 23:19:15 +03:00
Ghost deleted branch feat/reasoning-openai-compatible 2026-06-24 23:19:16 +03:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#177