mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
AIX: Transmit token stop errors, if provided
This commit is contained in:
@@ -905,9 +905,12 @@ export class ContentReassembler {
|
||||
/**
|
||||
* Stores raw termination data from the wire - classification deferred to finalizeReassembly()
|
||||
*/
|
||||
private onCGEnd({ terminationReason, tokenStopReason }: Extract<AixWire_Particles.ChatGenerateOp, { cg: 'end' }>): void {
|
||||
private onCGEnd({ terminationReason, tokenStopReason, tokenStopError }: Extract<AixWire_Particles.ChatGenerateOp, { cg: 'end' }>): void {
|
||||
this.S.terminationReason = terminationReason;
|
||||
this.S.dialectStopReason = tokenStopReason;
|
||||
// Vendor-composed stop error, surfaced as a complementary error fragment alongside the generic classification message
|
||||
if (tokenStopError)
|
||||
this._appendErrorFragment(tokenStopError);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -689,7 +689,7 @@ export namespace AixWire_Particles {
|
||||
|
||||
export type ChatControlOp =
|
||||
// | { cg: 'start' } // not really used for now
|
||||
| { cg: 'end', terminationReason: CGEndReason /* we know why we're sending 'end' */, tokenStopReason?: GCTokenStopReason /* we may or not have gotten a logical token stop reason from the dispatch */ }
|
||||
| { cg: 'end', terminationReason: CGEndReason /* we know why we're sending 'end' */, tokenStopReason?: GCTokenStopReason /* we may or not have gotten a logical token stop reason from the dispatch */, tokenStopError?: string /* optional vendor-composed human-readable detail paired with tokenStopReason */ }
|
||||
| { cg: 'issue', issueId: CGIssueId, issueText: string }
|
||||
| { cg: 'aix-info', ait: 'flow-cont' /* important: establishes a checkpoint */, text: string }
|
||||
| { cg: 'aix-retry-reset', rScope: 'srv-dispatch' | 'srv-op' | 'cli-ll', rClearStrategy: 'none' | 'since-checkpoint' | 'all', reason: string, attempt: number, maxAttempts: number, delayMs: number, causeHttp?: number, causeConn?: string }
|
||||
|
||||
@@ -56,6 +56,7 @@ export class ChatGenerateTransmitter implements IParticleTransmitter {
|
||||
|
||||
// Token stop reason
|
||||
private tokenStopReason: AixWire_Particles.GCTokenStopReason | undefined = undefined;
|
||||
private tokenStopError: string | undefined = undefined;
|
||||
|
||||
// Metrics
|
||||
private accMetrics: AixWire_Particles.CGSelectMetrics | undefined = undefined;
|
||||
@@ -105,6 +106,7 @@ export class ChatGenerateTransmitter implements IParticleTransmitter {
|
||||
cg: 'end',
|
||||
terminationReason: this.terminationReason,
|
||||
tokenStopReason: this.tokenStopReason, // See NOTE above - || (dispatchOrDialectIssue ? 'cg-issue' : 'ok'),
|
||||
...(this.tokenStopError && { tokenStopError: this.tokenStopError }),
|
||||
});
|
||||
// Keep this in a terminated state, so that every subsequent call will yield errors (not implemented)
|
||||
// this.terminationReason = null;
|
||||
@@ -201,12 +203,13 @@ export class ChatGenerateTransmitter implements IParticleTransmitter {
|
||||
this.setDialectEnded('issue-dialect');
|
||||
}
|
||||
|
||||
setTokenStopReason(reason: AixWire_Particles.GCTokenStopReason) {
|
||||
setTokenStopReason(reason: AixWire_Particles.GCTokenStopReason, errorText?: string) {
|
||||
if (SERVER_DEBUG_WIRE)
|
||||
console.log('|token-stop|', reason);
|
||||
console.log('|token-stop|', reason, errorText ?? '');
|
||||
if (this.tokenStopReason && this.tokenStopReason !== reason)
|
||||
console.warn(`[Aix.${this.prettyDialect}] setTokenStopReason('${reason}'): already has token stop reason '${this.tokenStopReason}' (overriding)`);
|
||||
this.tokenStopReason = reason;
|
||||
if (errorText) this.tokenStopError = errorText;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ export interface IParticleTransmitter {
|
||||
/** End the current part and flush it, which also calls `setDialectEnded('issue-dialect')` */
|
||||
setDialectTerminatingIssue(dialectText: string, symbol: string | null, serverLog: ParticleServerLogLevel): void;
|
||||
|
||||
/** Communicates the finish reason to the client - Data only, this does not do Control, like the above */
|
||||
setTokenStopReason(reason: AixWire_Particles.GCTokenStopReason): void;
|
||||
/** Communicates the finish reason to the client - Data only. Optional `errorText` is a vendor-composed string rendered as a complementary error fragment alongside the generic classification message. */
|
||||
setTokenStopReason(reason: AixWire_Particles.GCTokenStopReason, errorText?: string): void;
|
||||
|
||||
|
||||
// Parts data //
|
||||
|
||||
@@ -401,14 +401,10 @@ export function createAnthropicMessageParser(): ChatGenerateParseFunction {
|
||||
if (delta.container)
|
||||
_emitContainerState(pt, delta.container);
|
||||
|
||||
// -> Refusal details (structured) - surface category/explanation when stop_reason === 'refusal'
|
||||
if (delta.stop_reason === 'refusal' && delta.stop_details)
|
||||
_emitRefusalDetails(pt, delta.stop_details);
|
||||
|
||||
// -> Token Stop Reason
|
||||
const tokenStopReason = _fromAnthropicStopReason(delta.stop_reason, 'message_delta');
|
||||
if (tokenStopReason !== null)
|
||||
pt.setTokenStopReason(tokenStopReason);
|
||||
pt.setTokenStopReason(tokenStopReason, _formatAnthropicStopError(delta.stop_details));
|
||||
|
||||
// NOTE: we have more fields we're not parsing yet - https://platform.claude.com/docs/en/api/typescript/messages#message_delta_usage
|
||||
if (usage?.output_tokens && messageStartTime) {
|
||||
@@ -655,14 +651,10 @@ export function createAnthropicMessageParserNS(): ChatGenerateParseFunction {
|
||||
_createAnthropicPauseTurnContinuation(content, container?.id),
|
||||
);
|
||||
|
||||
// -> Refusal details (structured) - surface category/explanation when stop_reason === 'refusal'
|
||||
if (stop_reason === 'refusal' && stop_details)
|
||||
_emitRefusalDetails(pt, stop_details);
|
||||
|
||||
// -> Token Stop Reason (pause_turn already thrown above)
|
||||
const tokenStopReason = _fromAnthropicStopReason(stop_reason, 'parser_NS');
|
||||
if (tokenStopReason !== null)
|
||||
pt.setTokenStopReason(tokenStopReason);
|
||||
pt.setTokenStopReason(tokenStopReason, _formatAnthropicStopError(stop_details));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -690,18 +682,17 @@ function _emitContainerState(pt: IParticleTransmitter, container: { id: string;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Surface structured refusal details (stop_reason === 'refusal') as inline text.
|
||||
* Anthropic's streaming classifiers can intervene mid-generation; appending the category + explanation
|
||||
* as text lets the user see WHY the model refused without touching terminationReason (which message_stop
|
||||
* will set to 'done-dialect') - avoids a spurious override warning.
|
||||
*/
|
||||
function _emitRefusalDetails(pt: IParticleTransmitter, stopDetails: { type: 'refusal'; category?: 'cyber' | 'bio' | null; explanation?: string | null }): void {
|
||||
/** Compose a human-readable error string from Anthropic's stop_details. Returns undefined when nothing useful to surface. */
|
||||
function _formatAnthropicStopError(stopDetails: { type: string; category?: string | null; explanation?: string | null } | null | undefined): string | undefined {
|
||||
if (!stopDetails) return undefined;
|
||||
if (stopDetails.type !== 'refusal') {
|
||||
aixResilientUnknownValue('Anthropic', 'stopDetailsType', stopDetails.type);
|
||||
return undefined;
|
||||
}
|
||||
const parts: string[] = [];
|
||||
if (stopDetails.category) parts.push(`[${stopDetails.category}]`);
|
||||
if (stopDetails.explanation) parts.push(stopDetails.explanation);
|
||||
if (!parts.length) return;
|
||||
pt.appendText(`\n\n${IssueSymbols.PromptBlocked} **Refusal:** ${parts.join(' ')}`);
|
||||
return parts.length ? `Refusal: ${parts.join(' ')}` : undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -831,12 +831,11 @@ export namespace AnthropicWire_API_Message_Create {
|
||||
|
||||
/**
|
||||
* Structured stop details, paired with stop_reason. Currently only populated when stop_reason === 'refusal'.
|
||||
* - category: 'cyber' | 'bio' when the refusal maps to a named policy category, null otherwise
|
||||
* - explanation: human-readable explanation (NOT guaranteed stable), null when unavailable
|
||||
* Both `type` and `category` are loosely typed for forward-compat - parser warns on unknown `type`.
|
||||
*/
|
||||
const StopDetails_schema = z.object({
|
||||
type: z.literal('refusal'),
|
||||
category: z.enum(['cyber', 'bio']).nullish(),
|
||||
type: z.enum(['refusal']).or(z.string()),
|
||||
category: z.enum(['cyber', 'bio']).or(z.string()).nullish(),
|
||||
explanation: z.string().nullish(),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user