* Stream chat via RawGetChatMessage. * Used for models without a string UID (enum < 280 generally). * * @param {Array} messages - OpenAI-format messages * @param {number} modelEnum - Model enum value * @param {string} [modelName] - Optional model name * @param {object} opts - { onC
(messages, modelEnum, modelName, opts = {})
| 432 | * @param {object} opts - { onChunk, onEnd, onError } |
| 433 | */ |
| 434 | rawGetChatMessage(messages, modelEnum, modelName, opts = {}) { |
| 435 | acquireLsUseOrThrow(this.port); |
| 436 | try { |
| 437 | const { onChunk, onEnd, onError } = opts; |
| 438 | // Reuse the LS-scoped session_id instead of letting buildMetadata |
| 439 | // mint a fresh UUID on every call. A stable session per LS matches |
| 440 | // what a real Windsurf IDE instance sends (one session for the whole |
| 441 | // window's lifetime) and gives upstream fingerprinting less to latch |
| 442 | // onto. Cascade path already does this via lsEntry.sessionId; this |
| 443 | // closes the same gap for the legacy channel. |
| 444 | const lsEntry = getLsEntryByPort(this.port); |
| 445 | if (lsEntry && !lsEntry.sessionId) lsEntry.sessionId = randomUUID(); |
| 446 | const sessionId = lsEntry?.sessionId; |
| 447 | const proto = buildRawGetChatMessageRequest(this.apiKey, messages, modelEnum, modelName, sessionId); |
| 448 | const body = grpcFrame(proto); |
| 449 | |
| 450 | log.debug(`RawGetChatMessage: enum=${modelEnum} msgs=${messages.length}`); |
| 451 | |
| 452 | return new Promise((resolve, reject) => { |
| 453 | const chunks = []; |
| 454 | // Once the promise has settled, ignore any further stream events. The |
| 455 | // LS occasionally emits an error frame followed by a trailing onEnd; |
| 456 | // without this guard the second callback re-resolves/re-rejects. |
| 457 | let done = false; |
| 458 | |
| 459 | grpcStream(this.port, this.csrfToken, `${LS_SERVICE}/RawGetChatMessage`, body, { |
| 460 | onData: (payload) => { |
| 461 | if (done) return; |
| 462 | try { |
| 463 | const parsed = parseRawResponse(payload); |
| 464 | if (parsed.text) { |
| 465 | // Detect server-side errors returned as text |
| 466 | const errMatch = /^(permission_denied|failed_precondition|not_found|unauthenticated):/.test(parsed.text.trim()); |
| 467 | if (parsed.isError || errMatch) { |
| 468 | const err = new Error(parsed.text.trim()); |
| 469 | // Mark model-level errors so they don't count against the account |
| 470 | err.isModelError = /permission_denied|failed_precondition/.test(parsed.text); |
| 471 | if (err.isModelError) err.kind = 'model_error'; |
| 472 | done = true; |
| 473 | reject(err); |
| 474 | return; |
| 475 | } |
| 476 | chunks.push(parsed); |
| 477 | onChunk?.(parsed); |
| 478 | } |
| 479 | } catch (e) { |
| 480 | log.error('RawGetChatMessage parse error:', e.message); |
| 481 | } |
| 482 | }, |
| 483 | onEnd: () => { |
| 484 | if (done) return; |
| 485 | done = true; |
| 486 | onEnd?.(chunks); |
| 487 | resolve(chunks); |
| 488 | }, |
| 489 | onError: (err) => { |
| 490 | if (done) return; |
| 491 | done = true; |
no test coverage detected