Compare commits

..
39 changed files with 245 additions and 205 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ import { TypeSafeAI } from "@opencode/ai/providers"
const model = TypeSafeAI.configure().experimental.evaluation("jev-latest")
const program = Evaluation.evaluate({
const program = Evaluation.run({
model,
state: "I was charged twice. Please refund the duplicate payment.",
questions: {
+4 -4
View File
@@ -218,11 +218,11 @@ export function request(input: EvaluationRequest | EvaluationRequestInput) {
})
}
export function evaluate<const Model extends object, const Questions extends EvaluationQuestions>(
export function run<const Model extends object, const Questions extends EvaluationQuestions>(
input: EvaluationRequestInput<Model, Questions>,
): Effect.Effect<EvaluationResponseFor<Questions>, AIError, Service>
export function evaluate(input: EvaluationRequest): Effect.Effect<EvaluationResponse, AIError, Service>
export function evaluate(input: EvaluationRequest | EvaluationRequestInput) {
export function run(input: EvaluationRequest): Effect.Effect<EvaluationResponse, AIError, Service>
export function run(input: EvaluationRequest | EvaluationRequestInput) {
return Effect.try({
try: () => (input instanceof EvaluationRequest ? input : request(input)),
catch: (cause) =>
@@ -241,5 +241,5 @@ export function evaluate(input: EvaluationRequest | EvaluationRequestInput) {
export const Evaluation = {
request,
evaluate,
run,
} as const
+3 -3
View File
@@ -9,7 +9,7 @@ import { dynamicResponse } from "./lib/http.js"
describe("experimental Evaluation", () => {
it.effect("evaluates typed questions through System One", () =>
Effect.gen(function* () {
const response = yield* Evaluation.evaluate({
const response = yield* Evaluation.run({
model: TypeSafeAI.configure({
apiKey: "test",
baseURL: "https://typesafe.test/v1/",
@@ -116,7 +116,7 @@ describe("experimental Evaluation", () => {
)
it.effect("configures the OpenCode Zen System One endpoint", () =>
Evaluation.evaluate({
Evaluation.run({
model: OpenCodeZen.configure({ apiKey: "zen-key", baseURL: "https://zen.test/v1" }).experimental.evaluation(
"jev-1.13",
),
@@ -156,7 +156,7 @@ describe("experimental Evaluation", () => {
it.effect("rejects malformed questions before network I/O", () =>
Effect.gen(function* () {
const error = yield* Evaluation.evaluate({
const error = yield* Evaluation.run({
model: TypeSafeAI.experimental.evaluation("jev-latest"),
state: "hello",
questions: { score: { type: "score", instructions: "How much?", criteria: ["only"] } },
+3 -3
View File
@@ -31,7 +31,7 @@ void (true satisfies Choice)
void (true satisfies ClientRequirements)
Effect.gen(function* () {
const response = yield* Evaluation.evaluate({
const response = yield* Evaluation.run({
model: OpenCodeZen.experimental.evaluation("jev-1.13"),
state: ["hello"],
questions: { greeting: { type: "boolean", instructions: "Greeting?" } },
@@ -45,14 +45,14 @@ Effect.gen(function* () {
declare const route: EvaluationRoute<{ readonly temperature?: number }>
const custom = EvaluationModel.make({ id: "custom", provider: "custom", route })
Evaluation.evaluate({
Evaluation.run({
model: custom,
state: "hello",
questions: { ok: { type: "boolean", instructions: "OK?" } },
options: { temperature: 0.5 },
})
// @ts-expect-error Selected evaluation models retain their request option types.
Evaluation.evaluate({
Evaluation.run({
model: custom,
state: "hello",
questions: { ok: { type: "boolean", instructions: "OK?" } },
+1 -1
View File
@@ -40,7 +40,7 @@ describe("public exports", () => {
expect(TestLLM.layer).toBeFunction()
expect(TestLLM.testLayer).toBeFunction()
expect(TestLLM.Test.of).toBeFunction()
expect(Evaluation.evaluate).toBeFunction()
expect(Evaluation.run).toBeFunction()
expect(EvaluationClient.layer).toBeDefined()
expect(EvaluationClient.fetchLayer).toBeDefined()
})
@@ -63,7 +63,7 @@ const assertEvaluation = (
metadataKey: "typesafe" | "opencode",
) =>
Effect.gen(function* () {
const response = yield* Evaluation.evaluate({ model, state, questions })
const response = yield* Evaluation.run({ model, state, questions })
expect(response.model).toStartWith("jev-")
expect(response.answers.department.type).toBe("choice")
expect(response.answers.department.choice).toBe("billing")
@@ -703,7 +703,7 @@ function messageContent(
return {
type: "reasoning",
text: part.text,
native: jsonRecord(part.metadata),
state: jsonRecord(part.metadata),
time: part.time
? { created: part.time.start, ...(part.time.end === undefined ? {} : { completed: part.time.end }) }
: undefined,
@@ -1,9 +1,8 @@
import { OpenCode } from "@opencode/client"
import { Service } from "@opencode/client/effect/service"
import { Session } from "@opencode/schema/session"
import { SessionMessage } from "@opencode/schema/session-message"
import { SessionTransfer } from "@opencode/schema/session-transfer"
import { Effect, Option, Predicate, Schema } from "effect"
import { Effect, Option, Schema } from "effect"
import { EOL } from "node:os"
import path from "node:path"
import { Commands } from "../../commands"
@@ -24,13 +23,7 @@ export default Runtime.handler(
catch: (cause) =>
new Error(`Failed to read session data: ${cause instanceof Error ? cause.message : String(cause)}`),
})
const raw = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown))(text)
// Exports written before provider blobs were renamed to `native` still carry the old keys.
const data = yield* Schema.decodeUnknownEffect(SessionTransfer.Data)(
Predicate.isObject(raw) && Array.isArray(raw.messages)
? { ...raw, messages: raw.messages.map(SessionMessage.persisted) }
: raw,
)
const data = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(SessionTransfer.Data))(text)
const encoded = Schema.encodeSync(SessionTransfer.Data)(data)
const server = yield* ServerConnection.resolve({
server: Option.getOrUndefined(input.server),
+1 -1
View File
@@ -564,7 +564,7 @@ export async function runNonInteractivePrompt(input: Input) {
messageID: message.id,
type: "reasoning",
text,
metadata: item.native,
metadata: item.state,
time: { start: message.time.created, end: timestamp },
}
renderedReasoning.set(key, item.text)
+27 -23
View File
@@ -509,12 +509,12 @@ export type PromptAgentAttachment = { name: string; mention?: PromptMention }
export type PromptSkillAttachment = { id: string; name: string; text?: string; mention?: PromptMention }
export type SessionMessageAssistantText = { type: "text"; text: string; native?: SessionMessageProviderState }
export type SessionMessageAssistantText = { type: "text"; text: string; state?: SessionMessageProviderState }
export type SessionMessageAssistantReasoning = {
type: "reasoning"
text: string
native?: SessionMessageProviderState
state?: SessionMessageProviderState
time?: { created: number; completed?: number }
}
@@ -1354,6 +1354,15 @@ export type SessionToolCalled = {
}
}
export type SessionMessageAssistantText1 = { type: "text"; text: string; state?: SessionMessageProviderState1 }
export type SessionMessageAssistantReasoning1 = {
type: "reasoning"
text: string
state?: SessionMessageProviderState1
time?: { created: number; completed?: number }
}
export type ToolContent1 = ToolTextContent | ToolFileContent1
export type FormNumberField = {
@@ -1753,7 +1762,7 @@ export type SessionMessageCompactionCompleted = {
status: "completed"
reason: "auto" | "manual"
model?: ModelRef
native?: SessionMessageProviderState
providerState?: SessionMessageProviderState
summary: string
recent: string
providerContext?: SessionProviderContext
@@ -2219,7 +2228,7 @@ export type SessionMessageAssistant = {
snapshot?: { start?: string; end?: string; files?: Array<string> }
finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
rawFinish?: string
native?: SessionMessageProviderState
providerState?: SessionMessageProviderState
cost?: MoneyUSD
tokens?: TokenUsageInfo
error?: SessionStructuredError
@@ -2227,13 +2236,8 @@ export type SessionMessageAssistant = {
}
export type SessionMessageAssistantContentEncoded =
| { type: "text"; text: string; state?: SessionMessageProviderState1 }
| {
type: "reasoning"
text: string
time?: { created: number; completed?: number }
state?: SessionMessageProviderState1
}
| SessionMessageAssistantText1
| SessionMessageAssistantReasoning1
| SessionMessageAssistantTool1
export type FormInfo = { id: string; sessionID: string; title: string; metadata?: FormMetadata; fields: FormFields }
@@ -3098,11 +3102,11 @@ export type SessionImportInput = {
readonly agent: string
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly content: ReadonlyArray<
| { readonly type: "text"; readonly text: string; readonly native?: { readonly [x: string]: JsonValue } }
| { readonly type: "text"; readonly text: string; readonly state?: { readonly [x: string]: JsonValue } }
| {
readonly type: "reasoning"
readonly text: string
readonly native?: { readonly [x: string]: JsonValue }
readonly state?: { readonly [x: string]: JsonValue }
readonly time?: { readonly created: number; readonly completed?: number }
}
| {
@@ -3176,7 +3180,7 @@ export type SessionImportInput = {
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
readonly finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
readonly rawFinish?: string
readonly native?: { readonly [x: string]: JsonValue }
readonly providerState?: { readonly [x: string]: JsonValue }
readonly cost?: number
readonly tokens?: {
readonly input: number
@@ -3210,7 +3214,7 @@ export type SessionImportInput = {
readonly status: "completed"
readonly reason: "auto" | "manual"
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly native?: { readonly [x: string]: JsonValue }
readonly providerState?: { readonly [x: string]: JsonValue }
readonly summary: string
readonly recent: string
readonly providerContext?: {
@@ -3415,11 +3419,11 @@ export type SessionImportInput = {
readonly agent: string
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly content: ReadonlyArray<
| { readonly type: "text"; readonly text: string; readonly native?: { readonly [x: string]: JsonValue } }
| { readonly type: "text"; readonly text: string; readonly state?: { readonly [x: string]: JsonValue } }
| {
readonly type: "reasoning"
readonly text: string
readonly native?: { readonly [x: string]: JsonValue }
readonly state?: { readonly [x: string]: JsonValue }
readonly time?: { readonly created: number; readonly completed?: number }
}
| {
@@ -3493,7 +3497,7 @@ export type SessionImportInput = {
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
readonly finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
readonly rawFinish?: string
readonly native?: { readonly [x: string]: JsonValue }
readonly providerState?: { readonly [x: string]: JsonValue }
readonly cost?: number
readonly tokens?: {
readonly input: number
@@ -3527,7 +3531,7 @@ export type SessionImportInput = {
readonly status: "completed"
readonly reason: "auto" | "manual"
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly native?: { readonly [x: string]: JsonValue }
readonly providerState?: { readonly [x: string]: JsonValue }
readonly summary: string
readonly recent: string
readonly providerContext?: {
@@ -3732,11 +3736,11 @@ export type SessionImportInput = {
readonly agent: string
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly content: ReadonlyArray<
| { readonly type: "text"; readonly text: string; readonly native?: { readonly [x: string]: JsonValue } }
| { readonly type: "text"; readonly text: string; readonly state?: { readonly [x: string]: JsonValue } }
| {
readonly type: "reasoning"
readonly text: string
readonly native?: { readonly [x: string]: JsonValue }
readonly state?: { readonly [x: string]: JsonValue }
readonly time?: { readonly created: number; readonly completed?: number }
}
| {
@@ -3810,7 +3814,7 @@ export type SessionImportInput = {
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
readonly finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
readonly rawFinish?: string
readonly native?: { readonly [x: string]: JsonValue }
readonly providerState?: { readonly [x: string]: JsonValue }
readonly cost?: number
readonly tokens?: {
readonly input: number
@@ -3844,7 +3848,7 @@ export type SessionImportInput = {
readonly status: "completed"
readonly reason: "auto" | "manual"
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly native?: { readonly [x: string]: JsonValue }
readonly providerState?: { readonly [x: string]: JsonValue }
readonly summary: string
readonly recent: string
readonly providerContext?: {
+7 -8
View File
@@ -846,7 +846,7 @@ export function createData(config: CreateDataInput) {
existing.error = undefined
existing.finish = undefined
existing.rawFinish = undefined
existing.native = undefined
existing.providerState = undefined
existing.time.created = event.data.started
existing.time.streamed = undefined
existing.time.completed = undefined
@@ -880,7 +880,7 @@ export function createData(config: CreateDataInput) {
assistant.time.completed = event.created
assistant.finish = event.data.finish
assistant.rawFinish = event.data.rawFinish
assistant.native = event.data.providerState
assistant.providerState = event.data.providerState
assistant.cost = event.data.cost
assistant.tokens = event.data.tokens
if (event.data.snapshot) assistant.snapshot = { ...assistant.snapshot, end: event.data.snapshot }
@@ -892,7 +892,7 @@ export function createData(config: CreateDataInput) {
assistant.time.completed = event.created
assistant.finish = event.data.finish ?? "error"
assistant.rawFinish = event.data.rawFinish
assistant.native = event.data.providerState
assistant.providerState = event.data.providerState
assistant.error = event.data.error
assistant.retry = undefined
if (event.data.cost !== undefined && event.data.tokens !== undefined) {
@@ -914,7 +914,6 @@ export function createData(config: CreateDataInput) {
case "session.text.ended":
message.editText(event.data.sessionID, event.data.assistantMessageID, (text) => {
text.text = event.data.text
text.native = event.data.state
})
return
case "session.tool.input.started":
@@ -985,7 +984,7 @@ export function createData(config: CreateDataInput) {
assistant.content.push({
type: "reasoning",
text: "",
native: event.data.state,
state: event.data.state,
time: { created: event.created },
})
})
@@ -999,7 +998,7 @@ export function createData(config: CreateDataInput) {
message.editReasoning(event.data.sessionID, event.data.assistantMessageID, (reasoning) => {
reasoning.text = event.data.text
reasoning.time = { created: reasoning.time?.created ?? event.created, completed: event.created }
if (event.data.state !== undefined) reasoning.native = event.data.state
if (event.data.state !== undefined) reasoning.state = event.data.state
})
return
case "session.retry.scheduled":
@@ -1106,7 +1105,7 @@ export function createData(config: CreateDataInput) {
status: "completed",
reason: event.data.reason,
model: event.data.model,
native: event.data.providerState,
providerState: event.data.providerState,
providerContext: event.data.providerContext,
summary: event.data.text,
recent: event.data.recent,
@@ -1121,7 +1120,7 @@ export function createData(config: CreateDataInput) {
status: "completed",
reason: event.data.reason,
model: event.data.model,
native: event.data.providerState,
providerState: event.data.providerState,
providerContext: event.data.providerContext,
summary: event.data.text,
recent: event.data.recent,
@@ -135,7 +135,7 @@ test.each(["started", "cancelled", "failed"])(
status: "completed",
summary: "Summary",
model,
native: providerState,
providerState,
providerContext,
cost: 0.01,
tokens,
@@ -379,13 +379,13 @@ export function transformSession(input: TransformInput): TransformResult {
return []
const content = owned.flatMap((part): Array<Record<string, unknown>> => {
if (part.type === "text")
return [{ type: "text", text: part.text, ...(part.metadata ? { native: part.metadata } : {}) }]
return [{ type: "text", text: part.text, ...(part.metadata ? { state: part.metadata } : {}) }]
if (part.type === "reasoning")
return [
{
type: "reasoning",
text: part.text,
...(part.metadata ? { native: part.metadata } : {}),
...(part.metadata ? { state: part.metadata } : {}),
time: { created: part.time.start, ...(part.time.end === undefined ? {} : { completed: part.time.end }) },
},
]
+1 -1
View File
@@ -60,7 +60,7 @@ export const latestCompaction = Effect.fnUntraced(function* (
})
export const decodeMessageRow = (row: typeof SessionMessageTable.$inferSelect) =>
decode(SessionMessage.persisted({ ...row.data, id: row.id, type: row.type })).pipe(
decode({ ...row.data, id: row.id, type: row.type }).pipe(
Effect.tap((message) =>
SessionProviderContext.isCheckpoint(message)
? SessionProviderContext.validate(message.providerContext)
+1 -1
View File
@@ -125,7 +125,7 @@ const promotedFromMessage = Effect.fn("SessionInbox.promotedFromMessage")(functi
if (row === undefined) return undefined
if (row.session_id !== sessionID || (row.type !== "user" && row.type !== "synthetic"))
return yield* new LifecycleConflict({ id })
const message = decodeMessage(SessionMessage.persisted({ ...row.data, id: row.id, type: row.type }))
const message = decodeMessage({ ...row.data, id: row.id, type: row.type })
const base = { id, sessionID, time: { created: message.time.created }, delivery }
if (message.type === "user")
return User.make({
+9 -11
View File
@@ -83,9 +83,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
"session.message.content.updated": (event) =>
updateOwnedAssistant(event.data.messageID, (draft) => {
draft.content = castDraft(
Schema.decodeUnknownSync(Schema.Array(SessionMessage.AssistantContent))(
SessionMessage.persistedContent(event.data.content),
),
Schema.decodeUnknownSync(Schema.Array(SessionMessage.AssistantContent))(event.data.content),
)
}),
"session.usage.recorded": () => Effect.void,
@@ -224,7 +222,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
draft.error = undefined
draft.finish = undefined
draft.rawFinish = undefined
draft.native = undefined
draft.providerState = undefined
draft.time.created = DateTime.makeUnsafe(event.data.started)
draft.time.streamed = undefined
draft.time.completed = undefined
@@ -265,7 +263,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
draft.time.completed = created
draft.finish = event.data.finish
draft.rawFinish = event.data.rawFinish
draft.native = castDraft(event.data.providerState)
draft.providerState = castDraft(event.data.providerState)
draft.cost = event.data.cost
draft.tokens = event.data.tokens
projectTerminalSnapshot(draft, event)
@@ -276,7 +274,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
draft.time.completed = created
draft.finish = event.data.finish ?? "error"
draft.rawFinish = event.data.rawFinish
draft.native = castDraft(event.data.providerState)
draft.providerState = castDraft(event.data.providerState)
draft.error = castDraft(event.data.error)
draft.retry = undefined
if (event.data.cost !== undefined && event.data.tokens !== undefined) {
@@ -296,7 +294,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
const match = latestText(draft)
if (match) {
match.text = event.data.text
match.native = castDraft(event.data.state)
match.state = castDraft(event.data.state)
}
})
},
@@ -384,7 +382,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
SessionMessage.AssistantReasoning.make({
type: "reasoning",
text: "",
native: event.data.state,
state: event.data.state,
time: { created },
}),
),
@@ -397,7 +395,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
if (match) {
match.text = event.data.text
match.time = { created: match.time?.created ?? created, completed: created }
if (event.data.state !== undefined) match.native = event.data.state
if (event.data.state !== undefined) match.state = event.data.state
}
})
},
@@ -433,7 +431,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
metadata: event.metadata ? { ...current.metadata, ...event.metadata } : current.metadata,
reason: event.data.reason,
model: event.data.model,
native: event.data.providerState,
providerState: event.data.providerState,
summary: event.data.text,
providerContext: event.data.providerContext,
recent: event.data.recent,
@@ -450,7 +448,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
metadata: event.metadata,
reason: event.data.reason,
model: event.data.model,
native: event.data.providerState,
providerState: event.data.providerState,
summary: event.data.text,
providerContext: event.data.providerContext,
recent: event.data.recent,
+1 -1
View File
@@ -228,7 +228,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
function run(db: DatabaseService, event: MessageEvent) {
return Effect.gen(function* () {
const decodeRow = (row: typeof SessionMessageTable.$inferSelect) =>
decodeMessage(SessionMessage.persisted({ ...row.data, id: row.id, type: row.type }))
decodeMessage({ ...row.data, id: row.id, type: row.type })
const updateMessage = (message: SessionMessage.Info) => {
const encoded = encodeMessage(message)
const { id, type, ...data } = encoded
+1 -3
View File
@@ -107,9 +107,7 @@ const plan = Effect.fn("SessionRevert.plan")(function* (db: Database.Interface["
const decode = Schema.decodeUnknownEffect(SessionMessage.Info)
const files = new Map<RelativePath, Snapshot.ID>()
for (const row of rows) {
const message = yield* decode(SessionMessage.persisted({ ...row.data, id: row.id, type: row.type })).pipe(
Effect.orDie,
)
const message = yield* decode({ ...row.data, id: row.id, type: row.type }).pipe(Effect.orDie)
if (message.type !== "assistant" || !message.snapshot?.start) continue
for (const file of message.snapshot.files ?? [])
if (!files.has(file)) files.set(file, Snapshot.ID.make(message.snapshot.start))
@@ -162,7 +162,7 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider
text: item.text,
// Text can carry provider-bound state (e.g. Gemini thought signatures),
// which is only replayable against the model that produced it.
providerMetadata: reuseProviderMetadata ? providerMetadata(providerMetadataKey, item.native) : undefined,
providerMetadata: reuseProviderMetadata ? providerMetadata(providerMetadataKey, item.state) : undefined,
},
]
// Let the destination adapter handle readable reasoning after a model/provider switch.
@@ -172,7 +172,7 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider
{
type: "reasoning",
text: item.text,
providerMetadata: providerMetadata(providerMetadataKey, item.native),
providerMetadata: providerMetadata(providerMetadataKey, item.state),
},
]
: item.text.length > 0
+3 -3
View File
@@ -269,13 +269,13 @@ function sanitizeMessage(message: SessionMessage.Info): SessionMessage.Info {
return {
...content,
text: redact("text", message.id, content.text),
native: content.native ? { redacted: `text-native:${message.id}` } : undefined,
state: content.state ? { redacted: `text-state:${message.id}` } : undefined,
}
if (content.type === "reasoning")
return {
...content,
text: redact("reasoning", message.id, content.text),
native: content.native ? { redacted: `reasoning-native:${message.id}` } : undefined,
state: content.state ? { redacted: `reasoning-state:${message.id}` } : undefined,
}
return {
...content,
@@ -299,7 +299,7 @@ function sanitizeMessage(message: SessionMessage.Info): SessionMessage.Info {
summary: redact("compaction-summary", message.id, message.summary),
recent: redact("compaction-recent", message.id, message.recent),
...(message.status === "completed"
? { native: metadata("compaction-native", message.id, message.native) }
? { providerState: metadata("compaction-provider-state", message.id, message.providerState) }
: {}),
}
}
+9
View File
@@ -422,6 +422,15 @@ describe("MCP OAuth", () => {
expect(tokenRequests[0]?.get("grant_type")).toBe("refresh_token")
})
test("requests offline_access without forcing a consent prompt", async () => {
const { server } = authorizationServer({ scopes_supported: ["read", "offline_access"] })
const { url } = await Effect.runPromise(
Effect.scoped(start(server, { client_id: "client", scope: "read" })),
).finally(() => server.stop(true))
expect(url.searchParams.get("scope")).toBe("read offline_access")
expect(url.searchParams.has("prompt")).toBe(false)
})
test("forwards iss from the redirect so issuer-advertising servers can complete", async () => {
const { server } = authorizationServer({ authorization_response_iss_parameter_supported: true })
const result = await Effect.runPromise(
+4 -4
View File
@@ -1272,7 +1272,7 @@ describe("SessionTransfer", () => {
const runningCompactionID = SessionMessage.ID.create()
const completedCompactionID = SessionMessage.ID.create()
const model = Model.Ref.make({ id: Model.ID.make("model"), providerID: Provider.ID.make("provider") })
const native = { responseId: "summary-response" }
const providerState = { responseId: "summary-response" }
yield* transfer.import({
data: {
@@ -1328,7 +1328,7 @@ describe("SessionTransfer", () => {
status: "completed",
reason: "manual",
model,
native,
providerState,
summary: "summary",
recent: "recent",
time: { created: DateTime.makeUnsafe(9) },
@@ -1345,10 +1345,10 @@ describe("SessionTransfer", () => {
completedCompactionID,
])
expect(yield* Bus.latestSequence(db, sessionID)).toBe(4)
expect((yield* transfer.export({ sessionID })).messages.at(-1)).toMatchObject({ model, native })
expect((yield* transfer.export({ sessionID })).messages.at(-1)).toMatchObject({ model, providerState })
expect((yield* transfer.export({ sessionID, sanitize: true })).messages.at(-1)).toMatchObject({
model,
native: { redacted: `compaction-native:${completedCompactionID}` },
providerState: { redacted: `compaction-provider-state:${completedCompactionID}` },
})
}),
)
@@ -426,7 +426,7 @@ it.live("compaction hooks supply the summary instead of provider compaction", ()
status: "completed",
summary: "## Objective\n- hooked summary",
recent: "",
native: { responseId: "plugin" },
providerState: { responseId: "plugin" },
metadata: { plugin: "custom" },
tokens: { input: 10, output: 5 },
})
+2 -2
View File
@@ -690,7 +690,7 @@ describe("SessionProjector", () => {
type: "assistant",
finish: "stop",
rawFinish: "stop_sequence",
native: { response: "ended" },
providerState: { response: "ended" },
cost: Money.USD.make(1),
tokens: { input: 2, output: 3, reasoning: 4, cache: { read: 5, write: 6 } },
snapshot: { end: "snap_ended", files: ["src/ended.ts"] },
@@ -700,7 +700,7 @@ describe("SessionProjector", () => {
type: "assistant",
finish: "content-filter",
rawFinish: "blocked",
native: { response: "failed" },
providerState: { response: "failed" },
error: { type: "provider.invalid-request", message: "Failed" },
snapshot: { end: "snap_failed", files: ["src/failed.ts"] },
time: { completed: created },
@@ -72,7 +72,7 @@ describe("toLLMMessages", () => {
SessionMessage.AssistantReasoning.make({
type: "reasoning",
text: "",
native: { signature: "sig_1" },
state: { signature: "sig_1" },
}),
]),
],
@@ -711,7 +711,7 @@ Recent work
SessionMessage.AssistantReasoning.make({
type: "reasoning",
text: "Think",
native: { signature: "sig_1" },
state: { signature: "sig_1" },
}),
SessionMessage.AssistantTool.make({
type: "tool",
@@ -860,7 +860,7 @@ Recent work
SessionMessage.AssistantReasoning.make({
type: "reasoning",
text: "Think",
native: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
state: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
}),
],
time: { created, completed: created },
@@ -891,7 +891,7 @@ Recent work
SessionMessage.AssistantReasoning.make({
type: "reasoning",
text: "Think",
native: { signature: "signed" },
state: { signature: "signed" },
}),
],
time: { created, completed: created },
@@ -918,7 +918,7 @@ Recent work
SessionMessage.AssistantReasoning.make({
type: "reasoning",
text: "Partial thought",
native: { itemId: "rs_failed", reasoningEncryptedContent: null },
state: { itemId: "rs_failed", reasoningEncryptedContent: null },
}),
SessionMessage.AssistantTool.make({
type: "tool",
@@ -1016,7 +1016,7 @@ Recent work
SessionMessage.AssistantReasoning.make({
type: "reasoning",
text: "Visible thought",
native: { signature: "sig_old" },
state: { signature: "sig_old" },
}),
SessionMessage.AssistantTool.make({
type: "tool",
@@ -1110,7 +1110,7 @@ Recent work
SessionMessage.AssistantReasoning.make({
type: "reasoning",
text: "Visible thought",
native: { reasoningEncryptedContent: "encrypted" },
state: { reasoningEncryptedContent: "encrypted" },
}),
],
time: { created, completed: created },
@@ -1140,7 +1140,7 @@ Recent work
SessionMessage.AssistantText.make({
type: "text",
text: "Checking.",
native: { phase: "commentary" },
state: { phase: "commentary" },
}),
],
error: { type: "provider.unknown", message: "Interrupted after commentary" },
@@ -1171,7 +1171,7 @@ Recent work
SessionMessage.AssistantText.make({
type: "text",
text: "Checking.",
native: { phase: "commentary" },
state: { phase: "commentary" },
}),
],
time: { created, completed: created },
+8 -8
View File
@@ -2558,7 +2558,7 @@ describe("SessionRunnerLLM", () => {
expect(s.executions).toEqual(["x".repeat(4_000)])
expect((yield* s.messages).find((message) => message.type === "compaction")).toMatchObject({
model: { id: s.currentModel.id, providerID: s.currentModel.provider, variant },
native: { responseId: "summary" },
providerState: { responseId: "summary" },
})
// Compare wire content without the cache breakpoints that move to the new final message.
@@ -3508,12 +3508,12 @@ describe("SessionRunnerLLM", () => {
{
type: "reasoning",
text: "Signed thought",
native: { signature: "sig_1" },
state: { signature: "sig_1" },
},
{
type: "reasoning",
text: "Encrypted thought",
native: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
state: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
},
]),
])
@@ -3565,7 +3565,7 @@ describe("SessionRunnerLLM", () => {
{
type: "reasoning",
text: "thinking",
native: { reasoningField: "reasoning", reasoningDetails: details },
state: { reasoningField: "reasoning", reasoningDetails: details },
},
{ type: "text", text: "Hello world" },
]),
@@ -3609,7 +3609,7 @@ describe("SessionRunnerLLM", () => {
expect(yield* s.context).toMatchObject([
Expected.user("Check first"),
Expected.assistant({}, [
{ type: "text", text: "Checking.", native: { itemId: "msg_commentary", phase: "commentary" } },
{ type: "text", text: "Checking.", state: { itemId: "msg_commentary", phase: "commentary" } },
]),
])
@@ -4966,7 +4966,7 @@ describe("SessionRunnerLLM", () => {
type: "assistant",
finish: "stop",
rawFinish: "end_turn",
native: { responseId: "response-1", serviceTier: "priority" },
providerState: { responseId: "response-1", serviceTier: "priority" },
content: [Expected.text("Complete")],
},
])
@@ -4997,7 +4997,7 @@ describe("SessionRunnerLLM", () => {
type: "assistant",
finish: "content-filter",
rawFinish: "SAFETY",
native: {
providerState: {
responseId: "response-blocked",
refusal: { category: "safety", explanation: "Prompt blocked" },
},
@@ -5444,7 +5444,7 @@ describe("SessionRunnerLLM", () => {
{
type: "reasoning",
text: "",
native: { itemId: "rs_disconnected", reasoningEncryptedContent: "encrypted-state" },
state: { itemId: "rs_disconnected", reasoningEncryptedContent: "encrypted-state" },
},
]),
{ type: "synthetic", text: INCOMPLETE_STREAM_CONTINUATION },
+2 -2
View File
@@ -337,8 +337,8 @@ describe("V1Migration.transformSession", () => {
agent: "build",
model: { id: "model", providerID: "provider", variant: "fast" },
content: [
{ type: "text", text: "", native: { separator: true } },
{ type: "reasoning", text: "think", native: { provider: 1 }, time: { created: 21, completed: 22 } },
{ type: "text", text: "", state: { separator: true } },
{ type: "reasoning", text: "think", state: { provider: 1 }, time: { created: 21, completed: 22 } },
],
snapshot: { start: "snap_start", end: "snap_end", files: ["a.ts", "b.ts", "c.ts"] },
finish: "stop",
+4 -4
View File
@@ -17320,7 +17320,7 @@
"rawFinish": {
"type": "string"
},
"native": {
"providerState": {
"$ref": "#/components/schemas/Session.Message.ProviderState_4"
},
"cost": {
@@ -17349,7 +17349,7 @@
"text": {
"type": "string"
},
"native": {
"state": {
"$ref": "#/components/schemas/Session.Message.ProviderState_1"
},
"time": {
@@ -17396,7 +17396,7 @@
"text": {
"type": "string"
},
"native": {
"state": {
"$ref": "#/components/schemas/Session.Message.ProviderState"
}
},
@@ -17509,7 +17509,7 @@
"model": {
"$ref": "#/components/schemas/Model.Ref"
},
"native": {
"providerState": {
"$ref": "#/components/schemas/Session.Message.ProviderState_5"
},
"summary": {
+1 -1
View File
@@ -601,7 +601,7 @@ export namespace Compaction {
...Base,
reason: Started.data.fields.reason,
model: SessionMessage.CompactionCompleted.fields.model,
providerState: SessionMessage.CompactionCompleted.fields.native,
providerState: SessionMessage.CompactionCompleted.fields.providerState,
providerContext: SessionMessage.CompactionCompleted.fields.providerContext,
text: Schema.String,
recent: Schema.String,
+6 -43
View File
@@ -1,6 +1,6 @@
export * as SessionMessage from "./session-message.js"
import { Predicate, Schema, Struct } from "effect"
import { Schema } from "effect"
import { SessionProviderContext } from "./session-provider-context.js"
import { optional } from "./schema.js"
import { Content } from "./tool.js"
@@ -177,14 +177,14 @@ export interface AssistantText extends Schema.Schema.Type<typeof AssistantText>
export const AssistantText = Schema.Struct({
type: Schema.tag("text"),
text: Schema.String,
native: ProviderState.pipe(optional),
state: ProviderState.pipe(optional),
}).annotate({ identifier: "Session.Message.Assistant.Text" })
export interface AssistantReasoning extends Schema.Schema.Type<typeof AssistantReasoning> {}
export const AssistantReasoning = Schema.Struct({
type: Schema.tag("reasoning"),
text: Schema.String,
native: ProviderState.pipe(optional),
state: ProviderState.pipe(optional),
time: Schema.Struct({
created: DateTimeUtcFromMillis,
completed: DateTimeUtcFromMillis.pipe(optional),
@@ -196,18 +196,7 @@ export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning,
)
export type AssistantContent = AssistantText | AssistantReasoning | AssistantTool
/**
* Frozen at the shape older releases stored: text and reasoning carried their
* provider blob as `state`. Only replayed durable events still use it; read it
* through `persistedContent` before decoding as `AssistantContent`.
*/
export const AssistantContentEncoded = Schema.toEncoded(
Schema.Union([
Schema.Struct({ ...Struct.omit(AssistantText.fields, ["native"]), state: ProviderState.pipe(optional) }),
Schema.Struct({ ...Struct.omit(AssistantReasoning.fields, ["native"]), state: ProviderState.pipe(optional) }),
AssistantTool,
]).pipe(Schema.toTaggedUnion("type")),
).annotate({
export const AssistantContentEncoded = Schema.toEncoded(AssistantContent).annotate({
identifier: "Session.Message.AssistantContent.Encoded",
})
export type AssistantContentEncoded = typeof AssistantContentEncoded.Type
@@ -233,7 +222,7 @@ export const Assistant = Schema.Struct({
}).pipe(optional),
finish: FinishReason.pipe(optional),
rawFinish: Schema.String.pipe(optional),
native: ProviderState.pipe(optional),
providerState: ProviderState.pipe(optional),
cost: Money.USD.pipe(optional),
tokens: TokenUsage.Info.pipe(optional),
error: SessionError.Error.pipe(optional),
@@ -269,7 +258,7 @@ export const CompactionCompleted = Schema.Struct({
status: Schema.tag("completed"),
reason: Schema.Literals(["auto", "manual"]),
model: Model.Ref.pipe(optional),
native: ProviderState.pipe(optional),
providerState: ProviderState.pipe(optional),
summary: Schema.String,
recent: Schema.String,
providerContext: SessionProviderContext.Info.pipe(optional),
@@ -329,29 +318,3 @@ export type Info =
| Compaction
| Idle
export type Type = Info["type"]
/** Reads messages stored before provider blobs were renamed to `native`. Tool parts are unchanged. */
export function persisted(input: unknown) {
if (!Predicate.isObject(input)) return input
const message =
input.type === "assistant" || input.type === "compaction" ? rename(input, "providerState", "native") : input
if (message.type !== "assistant" || !Array.isArray(message.content)) return message
const content = persistedContent(message.content)
return content === message.content ? message : { ...message, content }
}
/** Reads assistant content stored before text and reasoning blobs were renamed to `native`. */
export function persistedContent(content: ReadonlyArray<unknown>) {
const next = content.map((part) => {
if (!Predicate.isObject(part) || (part.type !== "text" && part.type !== "reasoning")) return part
return rename(part, "state", "native")
})
return next.every((part, index) => part === content[index]) ? content : next
}
function rename(record: Record<string, unknown>, from: string, to: string) {
if (record[from] === undefined || record[to] !== undefined) return record
const value = record[from]
const rest = Object.fromEntries(Object.entries(record).filter(([key]) => key !== from))
return { ...rest, [to]: value }
}
@@ -257,8 +257,8 @@ describe("contract hygiene", () => {
text: "hello",
})
expect(
SessionMessage.AssistantReasoning.make({ type: "reasoning", text: "thinking", native: { id: "opaque" } }),
).toEqual({ type: "reasoning", text: "thinking", native: { id: "opaque" } })
SessionMessage.AssistantReasoning.make({ type: "reasoning", text: "thinking", state: { id: "opaque" } }),
).toEqual({ type: "reasoning", text: "thinking", state: { id: "opaque" } })
expect(
SessionMessage.AssistantTool.make({
type: "tool",
+1 -35
View File
@@ -23,48 +23,14 @@ test("assistant terminal diagnostics remain optional and round trip", () => {
...assistant,
finish: "content-filter",
rawFinish: "SAFETY",
native: { promptFeedback: { blockReason: "SAFETY" } },
providerState: { promptFeedback: { blockReason: "SAFETY" } },
}),
),
).toMatchObject({
finish: "content-filter",
rawFinish: "SAFETY",
native: { promptFeedback: { blockReason: "SAFETY" } },
})
const legacy = SessionMessage.persisted({
...assistant,
providerState: { promptFeedback: { blockReason: "SAFETY" } },
content: [{ type: "text", text: "hello", state: { signature: "sig" } }],
})
expect(decode(legacy)).toMatchObject({
native: { promptFeedback: { blockReason: "SAFETY" } },
content: [{ type: "text", native: { signature: "sig" } }],
})
expect(encode(decode(legacy))).not.toHaveProperty("providerState")
expect(SessionMessage.persisted(assistant)).toBe(assistant)
})
test("replayed content updates keep the stored provider blob shape", () => {
const content = [
{ type: "text", text: "hello", state: { signature: "sig" } },
{ type: "reasoning", text: "think", state: { id: "rs_1" }, time: { created: 1 } },
{ type: "tool", id: "call", name: "read", state: { status: "streaming", input: "" }, time: { created: 1 } },
] as const
const decoded = Schema.decodeUnknownSync(SessionEvent.MessageContentUpdated.data)({
sessionID: "ses_terminal",
messageID: "msg_terminal",
content,
})
expect(decoded.content).toEqual(content)
expect(
Schema.decodeUnknownSync(Schema.Array(SessionMessage.AssistantContent))(
SessionMessage.persistedContent(decoded.content),
),
).toMatchObject([
{ type: "text", native: { signature: "sig" } },
{ type: "reasoning", native: { id: "rs_1" } },
{ type: "tool", state: { status: "streaming" } },
])
})
test("failed steps only override the assistant finish for content filters", () => {
@@ -190,13 +190,13 @@ export const streamingDocument = document(
{
type: "reasoning",
text: "## Checking the current contract\n\nThe assistant content is nested on each current Session message.",
native: { phase: "streaming" },
state: { phase: "streaming" },
time: { created: STORY_TIME + 11_100 },
},
{
type: "text",
text: "I have the typed rows in place. Next I am checking the streaming presentation",
native: { phase: "streaming" },
state: { phase: "streaming" },
},
],
}),
@@ -47,7 +47,7 @@ function MermaidTimeline(props: { streaming: boolean }) {
"```mermaid\nsequenceDiagram\n Client->>Server: Send prompt\n Server->>Model: Generate response\n Model-->>Client: Response\n" +
(completed() ? "```" : ""),
].join("\n\n"),
...(completed() ? {} : { native: { phase: "streaming" } }),
...(completed() ? {} : { state: { phase: "streaming" } }),
},
],
},
+28 -1
View File
@@ -727,7 +727,7 @@ function App(props: { pair?: DialogPairCredentials }) {
title: "New session",
suggested: route.data.type === "session",
category: "Session",
slash: { name: "new", aliases: ["clear"] },
slash: { name: "new" },
run: () => {
const model = local.model.current()
const agent = local.agent.current()
@@ -749,6 +749,33 @@ function App(props: { pair?: DialogPairCredentials }) {
dialog.clear()
},
},
{
name: "session.clear",
title: "Clear session",
category: "Session",
slash: { name: "clear" },
run: () => {
const model = local.model.current()
const agent = local.agent.current()
const current =
route.data.type === "session"
? (data.session.get(route.data.sessionID)?.location ?? location.ref)
: undefined
sessionTabs.close()
route.navigate({
type: "home",
location: newSessionLocation(
config.data.session.new_location,
data.location.default().directory,
current,
location.error?.location,
),
})
if (agent) local.agent.set(agent.id)
if (model) local.model.set(model)
dialog.clear()
},
},
{
name: "open.menu",
title: "Open session or project",
+67
View File
@@ -0,0 +1,67 @@
import { expect, test } from "bun:test"
import { createAppFixture } from "./fixture/app"
import { directory, json } from "./fixture/tui-client"
import { tmpdir } from "./fixture/fixture"
const location = { directory, project: { id: "project", directory, canonical: directory } }
const session = {
id: "ses_clear",
title: "Session to clear",
projectID: "project",
location: { directory },
agent: "build",
model: { providerID: "provider", id: "model" },
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 0, updated: 0 },
}
function render(state: string) {
return createAppFixture({
state,
args: { sessionID: session.id },
config: { animations: false, tabs: { mode: "on" } },
fetch: (url) => {
if (url.pathname === "/api/fs/list") return json({ location, data: [] })
if (url.pathname === "/api/location") return json(location)
if (url.pathname === "/api/session") return json({ data: [session], cursor: {} })
if (url.pathname === `/api/session/${session.id}`) return json({ data: session })
if (/^\/api\/session\/[^/]+\/(message|inbox|permission)$/.test(url.pathname))
return json({ data: [], cursor: {} })
if (url.pathname === "/api/agent")
return json({ location, data: [{ id: "build", mode: "primary", hidden: false, permissions: [] }] })
if (url.pathname === "/api/provider") return json({ location, data: [{ id: "provider", name: "Provider" }] })
if (url.pathname === "/api/model")
return json({
location,
data: [{ id: "model", providerID: "provider", name: "Model", variants: [] }],
})
},
})
}
test("/clear replaces the active session tab", async () => {
await using state = await tmpdir()
await using setup = await render(state.path)
await setup.waitForFrame((frame) => frame.includes("Session to clear"))
await setup.mockInput.typeText("/clear")
setup.mockInput.pressEnter()
const frame = await setup.waitForFrame((frame) => !frame.includes("Session to clear"))
expect(frame).not.toContain("Session to clear")
})
test("/new keeps the active session tab", async () => {
await using state = await tmpdir()
await using setup = await render(state.path)
await setup.waitForFrame((frame) => frame.includes("Session to clear"))
await setup.mockInput.typeText("/new")
setup.mockInput.pressEnter()
const frame = await setup.waitForFrame(
(frame) => frame.includes("New session") && frame.includes("Session to clear"),
)
expect(frame).toContain("Session to clear")
})
@@ -1,5 +1,5 @@
diff --git a/dist/index.cjs b/dist/index.cjs
index 635f1c0134274c89e7efbcae2adf3d9ecba575ee..0054b779ff7b550ecf6d4008e177a888003f9c92 100644
index 635f1c0134274c89e7efbcae2adf3d9ecba575ee..5eaa7b451c8c7e12e51f61c2a835ff6f7a8ea0ae 100644
--- a/dist/index.cjs
+++ b/dist/index.cjs
@@ -977,7 +977,6 @@ async function discoverMetadataWithFallback(serverUrl, wellKnownType, fetchFn, o
@@ -10,8 +10,16 @@ index 635f1c0134274c89e7efbcae2adf3d9ecba575ee..0054b779ff7b550ecf6d4008e177a888
}
let response = await tryMetadataDiscovery(url, protocolVersion, fetchFn);
if (!opts?.metadataUrl && shouldAttemptFallback(response, issuer.pathname)) response = await tryMetadataDiscovery(new URL(`/.well-known/${wellKnownType}`, issuer), protocolVersion, fetchFn);
@@ -1158,7 +1157,6 @@ async function startAuthorization(authorizationServerUrl, { metadata, clientInfo
authorizationUrl.searchParams.set("redirect_uri", String(redirectUrl));
if (state) authorizationUrl.searchParams.set("state", state);
if (scope) authorizationUrl.searchParams.set("scope", scope);
- if (scope?.split(" ").includes("offline_access")) authorizationUrl.searchParams.append("prompt", "consent");
if (resource) authorizationUrl.searchParams.set("resource", resource.href);
return {
authorizationUrl,
diff --git a/dist/index.mjs b/dist/index.mjs
index f02ce3ca394e826fc27c9848dbe9a214bf6ba7a9..8e27c9320e14ba8816fda752f250f6ddb1cc7ed0 100644
index f02ce3ca394e826fc27c9848dbe9a214bf6ba7a9..73a93a066c3540e131fa0770a74b4a7e8d9343a0 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -974,7 +974,6 @@ async function discoverMetadataWithFallback(serverUrl, wellKnownType, fetchFn, o
@@ -22,3 +30,11 @@ index f02ce3ca394e826fc27c9848dbe9a214bf6ba7a9..8e27c9320e14ba8816fda752f250f6dd
}
let response = await tryMetadataDiscovery(url, protocolVersion, fetchFn);
if (!opts?.metadataUrl && shouldAttemptFallback(response, issuer.pathname)) response = await tryMetadataDiscovery(new URL(`/.well-known/${wellKnownType}`, issuer), protocolVersion, fetchFn);
@@ -1155,7 +1154,6 @@ async function startAuthorization(authorizationServerUrl, { metadata, clientInfo
authorizationUrl.searchParams.set("redirect_uri", String(redirectUrl));
if (state) authorizationUrl.searchParams.set("state", state);
if (scope) authorizationUrl.searchParams.set("scope", scope);
- if (scope?.split(" ").includes("offline_access")) authorizationUrl.searchParams.append("prompt", "consent");
if (resource) authorizationUrl.searchParams.set("resource", resource.href);
return {
authorizationUrl,
+4 -4
View File
@@ -17320,7 +17320,7 @@
"rawFinish": {
"type": "string"
},
"native": {
"providerState": {
"$ref": "#/components/schemas/Session.Message.ProviderState_4"
},
"cost": {
@@ -17349,7 +17349,7 @@
"text": {
"type": "string"
},
"native": {
"state": {
"$ref": "#/components/schemas/Session.Message.ProviderState_1"
},
"time": {
@@ -17396,7 +17396,7 @@
"text": {
"type": "string"
},
"native": {
"state": {
"$ref": "#/components/schemas/Session.Message.ProviderState"
}
},
@@ -17509,7 +17509,7 @@
"model": {
"$ref": "#/components/schemas/Model.Ref"
},
"native": {
"providerState": {
"$ref": "#/components/schemas/Session.Message.ProviderState_5"
},
"summary": {
+4 -4
View File
@@ -17320,7 +17320,7 @@
"rawFinish": {
"type": "string"
},
"native": {
"providerState": {
"$ref": "#/components/schemas/Session.Message.ProviderState_4"
},
"cost": {
@@ -17349,7 +17349,7 @@
"text": {
"type": "string"
},
"native": {
"state": {
"$ref": "#/components/schemas/Session.Message.ProviderState_1"
},
"time": {
@@ -17396,7 +17396,7 @@
"text": {
"type": "string"
},
"native": {
"state": {
"$ref": "#/components/schemas/Session.Message.ProviderState"
}
},
@@ -17509,7 +17509,7 @@
"model": {
"$ref": "#/components/schemas/Model.Ref"
},
"native": {
"providerState": {
"$ref": "#/components/schemas/Session.Message.ProviderState_5"
},
"summary": {