@antseed/cli 0.1.155 → 0.1.156

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@ import test from 'node:test';
8
8
  import { ANTSEED_BUYER_FAULT_ERROR_CODE, ANTSEED_FAULT_ATTRIBUTION_HEADER, CONNECTION_CAPABILITY_COOPERATIVE_CLOSE_V1, CONNECTION_CAPABILITY_RELAYS_SWEEPS_V1, buyerFault, computeOnChainReputationScore, } from '@antseed/node';
9
9
  import { DEFAULT_BUYER_PEER_REFRESH_INTERVAL_MS } from '../config/defaults.js';
10
10
  import { BuyerProxy, isModelNotFoundResponse, makeVerifierReach, mergeJsonStateFile, parsePeerPinnedService, parsePersistedPeers, rewritePeerPinnedServiceInBody, sanitizePeerBuyerFaultMarker, selectCandidatePeersForRouting, substituteRoutedModelAlias, sweepStaleStateTmpFiles, } from './buyer-proxy.js';
11
- import { extractRequestedService, overrideRoutedModelInBody, SYSTEM_ROUTED_MODEL_HEADER } from './request-utils.js';
11
+ import { extractRequestedService, overrideRoutedModelInBody, SYSTEM_ROUTED_MODEL_HEADER, } from './request-utils.js';
12
12
  function makePeer(seed, providers) {
13
13
  const repeated = (seed.repeat(40) + 'a'.repeat(40)).slice(0, 40);
14
14
  return {
@@ -1069,6 +1069,64 @@ test('pinned proxy request reports when the pinned peer is not discoverable', as
1069
1069
  assert.match(res.body, /is not reachable right now/);
1070
1070
  assert.match(res.body, /It may be offline, not announcing, or temporarily unreachable/);
1071
1071
  });
1072
+ test('pinned proxy request surfaces a 403 without failing over to another peer', async () => {
1073
+ // An explicit pin is a hard constraint: even with another peer serving the
1074
+ // same model, the pinned peer's error is surfaced rather than re-routed.
1075
+ const pinnedPeer = makePeer('a', ['openai']);
1076
+ pinnedPeer.providerServiceApiProtocols = {
1077
+ openai: { services: { 'gpt-5': ['openai-chat-completions'] } },
1078
+ };
1079
+ const otherPeer = makePeer('b', ['openai']);
1080
+ otherPeer.providerServiceApiProtocols = {
1081
+ openai: { services: { 'GPT 5': ['openai-chat-completions'] } },
1082
+ };
1083
+ const proxy = makeBuyerProxyWithPeers([pinnedPeer, otherPeer], [pinnedPeer, otherPeer], permissiveRouter());
1084
+ const attempts = [];
1085
+ proxy._node.sendRequest = async (peer, request) => {
1086
+ attempts.push(peer.peerId);
1087
+ return {
1088
+ requestId: request.requestId,
1089
+ statusCode: 403,
1090
+ headers: { 'content-type': 'text/html' },
1091
+ body: Buffer.from('<html><body><h1>403 Forbidden</h1></body></html>'),
1092
+ };
1093
+ };
1094
+ const res = await invokeProxy(proxy, makeProxyRequest({
1095
+ headers: { 'x-antseed-pin-peer': pinnedPeer.peerId },
1096
+ body: { model: 'gpt-5', messages: [] },
1097
+ }));
1098
+ assert.equal(res.statusCode, 403);
1099
+ assert.deepEqual(attempts, [pinnedPeer.peerId]);
1100
+ });
1101
+ test('model-only routing fails over to the next peer after a 401 or 403', async () => {
1102
+ for (const statusCode of [401, 403]) {
1103
+ const first = makePeer('a', ['openai']);
1104
+ first.reputationScore = 95;
1105
+ first.providerServiceApiProtocols = {
1106
+ openai: { services: { 'gpt-5': ['openai-chat-completions'] } },
1107
+ };
1108
+ const second = makePeer('b', ['openai']);
1109
+ second.reputationScore = 80;
1110
+ second.providerServiceApiProtocols = {
1111
+ openai: { services: { 'GPT 5': ['openai-chat-completions'] } },
1112
+ };
1113
+ const proxy = makeBuyerProxyWithPeers([first, second], [first, second], permissiveRouter());
1114
+ const attempts = [];
1115
+ proxy._node.sendRequest = async (peer, request) => {
1116
+ attempts.push(peer.peerId);
1117
+ return {
1118
+ requestId: request.requestId,
1119
+ statusCode: peer.peerId === first.peerId ? statusCode : 200,
1120
+ headers: { 'content-type': 'application/json' },
1121
+ body: Buffer.from(JSON.stringify({ peerId: peer.peerId })),
1122
+ };
1123
+ };
1124
+ const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'gpt-5', messages: [] } }));
1125
+ assert.equal(res.statusCode, 200, `expected failover to succeed for upstream ${statusCode}`);
1126
+ assert.deepEqual(attempts, [first.peerId, second.peerId]);
1127
+ assert.equal(JSON.parse(res.body).peerId, second.peerId);
1128
+ }
1129
+ });
1072
1130
  test('pinned proxy request rewrites a canonical alias to the advertised service id', async () => {
1073
1131
  const pinnedPeer = makePeer('a', ['openai']);
1074
1132
  pinnedPeer.providerServiceApiProtocols = {
@@ -2514,6 +2572,59 @@ test('title request racing ahead of the first turn does not name the chat', asyn
2514
2572
  },
2515
2573
  }));
2516
2574
  assert.equal(store.get('claude-code:cc_race'), null);
2575
+ // Factory/Droid has no session header. Its main turn and concurrent
2576
+ // one-shot title request therefore hash to different synthetic keys. The
2577
+ // system-role title instruction must keep the second key out of the list.
2578
+ await invokeProxy(proxy, makeProxyRequest({
2579
+ path: '/v1/chat/completions',
2580
+ headers: { originator: 'droid', 'user-agent': 'factory-cli/0.202.0' },
2581
+ body: {
2582
+ model: 'antseed',
2583
+ messages: [
2584
+ { role: 'system', content: 'You are Droid, an AI software engineering agent.' },
2585
+ { role: 'user', content: 'wowow' },
2586
+ ],
2587
+ },
2588
+ }));
2589
+ await invokeProxy(proxy, makeProxyRequest({
2590
+ path: '/v1/chat/completions',
2591
+ headers: {
2592
+ originator: 'droid',
2593
+ 'user-agent': 'factory-cli/0.202.0',
2594
+ },
2595
+ body: {
2596
+ model: 'antseed',
2597
+ messages: [
2598
+ {
2599
+ role: 'system',
2600
+ content: `Shared provider compatibility preamble.
2601
+ You are a helper that generates concise session titles for a session picker.
2602
+ Input: one user message from the start of a session.`,
2603
+ },
2604
+ { role: 'user', content: 'wowow' },
2605
+ ],
2606
+ },
2607
+ }));
2608
+ assert.equal(store.list().filter((conversation) => conversation.tool === 'droid').length, 1);
2609
+ // The exact helper marker describes housekeeping regardless of which
2610
+ // integration sends it, so it does not create another conversation row.
2611
+ await invokeProxy(proxy, makeProxyRequest({
2612
+ path: '/v1/chat/completions',
2613
+ headers: { originator: 'other-agent', 'user-agent': 'other-agent/1.0' },
2614
+ body: {
2615
+ model: 'antseed',
2616
+ messages: [
2617
+ {
2618
+ role: 'system',
2619
+ content: `Shared provider compatibility preamble.
2620
+ You are a helper that generates concise session titles for a session picker.
2621
+ Input: one user message from the start of a session.`,
2622
+ },
2623
+ { role: 'user', content: 'wowow' },
2624
+ ],
2625
+ },
2626
+ }));
2627
+ assert.equal(store.list().filter((conversation) => conversation.tool === 'other-agent').length, 0);
2517
2628
  // The real first turn creates the conversation afterwards.
2518
2629
  await invokeProxy(proxy, makeProxyRequest({
2519
2630
  path: '/v1/messages',