Compare commits

..
Author SHA1 Message Date
james.maesandClaude Fable 5.1 2ad5e9068d fix(api): say NOT_CONFIGURED, with what to configure, when no model provider is usable (#82)
GET /config checked the user's permission before the provider, so a
server with no model provider told every user to ask for access, and
calls answered 403 NO_PERMISSION. Server-wide causes now come first
(DISABLED, NOT_CONFIGURED, NO_PERMISSION, KILL_SWITCH), the probe has a
`message` naming the cause (for NOT_CONFIGURED: add an enabled row on
Assistant > Providers, e.g. Anthropic with ANTHROPIC_API_KEY set, or a
local OpenAI-compatible server such as Ollama), and a user without
access on such a server gets 503 NOT_CONFIGURED instead of 403. The code
stays NOT_CONFIGURED because the Next UI and the spec already know it.
UI API extension assistant 1.7.0 (additive field).

The existing OpenAI-compatible adapter was verified live against a local
Ollama llama3.3:70b-instruct-q4_K_M (text and a tool call), so no new
provider module is needed for a keyless local model; the README shows
the Ollama row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 11:07:21 -06:00
6 changed files with 0 additions and 171 deletions

No files matched your search

-25
View File
@@ -1,25 +0,0 @@
# Standard QRun release caller (v2, thin contract): every push and manual dispatch delegates to the protected qrun-release-v1 channel. Do not edit per repository.
name: qrun-release
on:
push: {}
workflow_dispatch: {}
permissions:
contents: read
actions: read
jobs:
release:
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/java-docker.yaml@qrun-release-v1
with:
runner: linux-small
docker-runner: container-build
browsers: true
browser-runner: browser-build
postpublish:
if: ${{ needs.release.outputs.capture == 'true' }}
needs: release
permissions:
contents: write
actions: write
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/qrun-source-version.yaml@qrun-release-v1
with:
runner: linux-small
-72
View File
@@ -1,72 +0,0 @@
name: Release candidate
'on':
workflow_dispatch:
inputs:
dry_run:
description: Plan a cut without changing release state
type: boolean
default: false
finalize:
description: Retry finalization of an existing cut; do not cut another RC
type: boolean
default: false
recover:
description: Recover the same published RC after a deployment timeout; never
reupload
type: boolean
default: false
circle_build:
description: Original release run id (recovery only)
type: string
default: ''
merge_sha:
description: Existing cut merge_sha (finalize retry only)
type: string
default: ''
bump_sha:
description: Existing cut bump_sha (finalize retry only)
type: string
default: ''
branch:
description: Existing cut branch (finalize retry only)
type: string
default: ''
version:
description: Existing cut version (finalize retry only)
type: string
default: ''
pull_request:
types:
- closed
branches:
- develop
permissions:
contents: read
jobs:
release-rc:
# The old qbit-rc-published repository_dispatch is replaced by the already-proven workflow_dispatch
# completion: the publisher's own ActionsWRITE job dispatches this workflow with finalize=true plus
# merge_sha/bump_sha/branch/version (same inputs as the manual finalize retry).
permissions:
contents: write
actions: read
pull-requests: write
if: github.event_name == 'workflow_dispatch'
|| (github.event_name == 'pull_request' && github.event.pull_request.merged
== true && github.event.pull_request.base.ref == 'develop' && contains(github.event.pull_request.labels.*.name,
'release:rc'))
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/qbit-release-rc.yaml@qrun-release-v1
with:
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false
}}
finalize: ${{ github.event_name == 'workflow_dispatch' && inputs.finalize }}
recover: ${{ github.event_name == 'workflow_dispatch' && inputs.recover || false
}}
circle_build: ${{ github.event_name == 'workflow_dispatch' && inputs.circle_build
|| '' }}
merge_sha: ${{ inputs.merge_sha || '' }}
bump_sha: ${{ inputs.bump_sha || '' }}
branch: ${{ inputs.branch || '' }}
version: ${{ inputs.version || '' }}
secrets:
QRUN_TAG_SIGNING_KEY: ${{ secrets.QRUN_TAG_SIGNING_KEY }}
-5
View File
@@ -1,9 +1,5 @@
# Changelog
## Unreleased - Answer visible at the end of the stream
- The turn's redacted transcript (the question and answer parts, pending actions, stored events) is now written before `message.end` is sent. The panel reloads the conversation the moment the stream ends, and used to read the empty placeholders (`[]`) the transcript redactor stores until its flush, so the newest question and answer showed as empty bubbles until the panel was closed and reopened.
## Unreleased - Clear reason without a model provider (UI API extension 1.7.0)
- `GET /config` checks the server-wide causes before the user's permission: `DISABLED`, then `NOT_CONFIGURED` (no usable model provider), then `NO_PERMISSION`, then `KILL_SWITCH`. A server with no provider no longer tells users to ask for access (#82).
@@ -38,7 +34,6 @@
- Provider SPI: `ModelPart.Type.IMAGE` (`mediaType`, base64 `data`), mapped by the Anthropic, Bedrock and OpenAI-compatible providers. Claude models report vision. `ModelPart.toString()` never includes text or image data.
- `GET /config`: `features.attachments`, `limits.maxAttachmentBytes`, `limits.maxAttachments`, `limits.attachmentTypes`.
- System prompt: when `create_record` / `update_record` are offered, the model proposes the change instead of saying it cannot create records.
||||||| parent of c5125f9 (fix(assistant): store the transcript before message.end so the panel shows the answer)
## Unreleased - Optional provider health
@@ -1343,17 +1343,6 @@ public class TurnEngine implements Runnable
String finalErrorText = errorText;
safely(() -> emit(TurnEvent.ERROR, map("code", finalErrorCode, "message", finalErrorText)));
}
///////////////////////////////////////////////////////////////////////
// the stream ends at message.end and the panel reloads the //
// conversation at once, so the redacted transcript (message parts, //
// pending actions, events) must be stored before that event is sent //
///////////////////////////////////////////////////////////////////////
if(!alreadyTerminal)
{
safely(this::persistPendingEvents);
safely(() -> transcript.flush(store));
}
safely(() -> emit(TurnEvent.MESSAGE_END, map("messageId", assistantMessage.getId(), "status", finalStatus, "stopReason", finalStopReason)));
if(!alreadyTerminal)
{
@@ -141,27 +141,6 @@ class TurnEngineTest extends BaseAssistantTest
/*******************************************************************************
** The panel reloads the conversation as soon as message.end arrives, so the
** question and the answer must already be stored then, not after it.
*******************************************************************************/
@Test
void testMessagesAreStoredBeforeMessageEnd() throws Exception
{
provider.thenText("Four posts are published.");
TurnRunner.Outcome outcome = runner.run(EDITOR, "how many published posts?");
assertThat(outcome.end()).containsEntry("status", "COMPLETE");
List<AssistantMessage> stored = runner.messagesAtEnd();
assertThat(stored).hasSize(2);
assertThat(stored).extracting(AssistantMessage::getPartsJson).allSatisfy(parts -> assertThat(parts).isNotEqualTo("[]"));
assertThat(stored.get(0).getPartsJson()).contains("how many published posts?");
assertThat(stored.get(1).getPartsJson()).contains("Four posts are published.");
}
/*******************************************************************************
** Parallel tool calls: all results in ONE user message, in order.
*******************************************************************************/
@@ -27,7 +27,6 @@ import com.kingsrook.qbits.assistant.AssistantRuntime;
import com.kingsrook.qbits.assistant.api.AssistantApi;
import com.kingsrook.qbits.assistant.engine.RunningTurn;
import com.kingsrook.qbits.assistant.engine.TurnEvent;
import com.kingsrook.qbits.assistant.model.AssistantMessage;
import com.kingsrook.qbits.assistant.model.AssistantToolCall;
import com.kingsrook.qbits.assistant.persistence.AssistantStore;
import com.kingsrook.qqq.backend.core.context.QContext;
@@ -48,8 +47,6 @@ public class TurnRunner
private final QInstance qInstance;
private final AssistantQBitConfig config;
private List<AssistantMessage> messagesAtEnd = List.of();
/*******************************************************************************
@@ -172,10 +169,6 @@ public class TurnRunner
{
List<TurnEvent> batch = running.getLog().after(last, 1000);
events.addAll(batch);
if(batch.stream().anyMatch(e -> TurnEvent.MESSAGE_END.equals(e.getType())))
{
messagesAtEnd = messagesOf(userId, convId);
}
if(!batch.isEmpty())
{
last = batch.get(batch.size() - 1).getSeq();
@@ -201,36 +194,6 @@ public class TurnRunner
/*******************************************************************************
** The conversation's stored messages, read the moment the run's message.end
** was seen: what the panel finds when it reloads the conversation at the end
** of the stream.
*******************************************************************************/
public List<AssistantMessage> messagesAtEnd()
{
return (messagesAtEnd);
}
/*******************************************************************************
** The stored messages of a conversation (read as the user).
*******************************************************************************/
private List<AssistantMessage> messagesOf(String userId, Integer conversationId) throws Exception
{
QContext.init(qInstance, AssistantTestFixture.ownerSession(userId, config));
try
{
return (new AssistantStore().listMessages(userId, conversationId, null, 50));
}
finally
{
QContext.clear();
}
}
/*******************************************************************************
** The audited tool calls of a turn (read as the user).
*******************************************************************************/