fix(studio): handle expired OpenAI shell-tool containers without surfacing error in chat (#5547)

* fix(studio): transparent retry on expired OpenAI shell container

* fix(studio): drop expired OpenAI containers before send
This commit is contained in:
Roland Tannous 2026-05-18 16:47:57 +04:00 committed by GitHub
commit c0cc975c91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 854 additions and 607 deletions

View file

@ -16,7 +16,10 @@ import {
validateModel,
} from "./chat-api";
import { pickFriendlyContainerName } from "../lib/friendly-names";
import { createOpenAIContainer } from "./openai-containers";
import {
createOpenAIContainer,
listOpenAIContainers,
} from "./openai-containers";
import {
encryptProviderApiKey,
isProviderKeyRotationError,
@ -1046,6 +1049,41 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
openaiCodeExecContainerId = null;
anthropicCodeExecContainerId = null;
}
// Pre-send container validation (OpenAI only). The list
// endpoint already filters status==="expired" server-side
// (studio/backend/routes/inference.py — list_openai_containers),
// so membership in this set means "OpenAI will accept it
// as container_reference". A stale id silently dropped here
// falls through to the inheritance + lazy-create logic
// below, so the user never sees "Container is expired" in
// the chat thread. On list-call failure we leave
// activeContainerIds null and skip validation — the
// backend's transparent retry path is the safety net for
// that case.
let activeContainerIds: Set<string> | null = null;
if (externalProvider.providerType === "openai") {
try {
const list = await listOpenAIContainers({
apiKey: externalApiKey,
baseUrl: externalProvider.baseUrl || null,
});
activeContainerIds = new Set(list.map((c) => c.id));
} catch {
activeContainerIds = null;
}
if (
activeContainerIds &&
openaiCodeExecContainerId &&
!activeContainerIds.has(openaiCodeExecContainerId)
) {
void db.threads
.update(resolvedThreadId, {
openaiCodeExecContainerId: null,
})
.catch(() => {});
openaiCodeExecContainerId = null;
}
}
// Cross-thread inheritance: when the active thread has
// no container yet, default to the one most recently
// used on *any* other thread (provider-scoped).
@ -1066,15 +1104,27 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
.toArray();
for (const t of others) {
if (t.id === resolvedThreadId) continue;
if (t.openaiCodeExecContainerId) {
openaiCodeExecContainerId = t.openaiCodeExecContainerId;
if (!t.openaiCodeExecContainerId) continue;
// Skip inherited ids that are not in the active
// container set — they would 400 on send. Also
// null them on the source thread so the next
// inheritance pass doesn't re-pick the same dead id.
if (
activeContainerIds &&
!activeContainerIds.has(t.openaiCodeExecContainerId)
) {
void db.threads
.update(resolvedThreadId, {
openaiCodeExecContainerId,
})
.update(t.id, { openaiCodeExecContainerId: null })
.catch(() => {});
break;
continue;
}
openaiCodeExecContainerId = t.openaiCodeExecContainerId;
void db.threads
.update(resolvedThreadId, {
openaiCodeExecContainerId,
})
.catch(() => {});
break;
}
} catch {
/* fall through to lazy-create below */