Skip to content

Commit d18e2db

Browse files
authored
Remove legacy discovery fallback for 0.3.3 (#5)
1 parent 813dadc commit d18e2db

10 files changed

Lines changed: 198 additions & 273 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## 0.3.3
4+
5+
### Changed
6+
7+
- Discovery now accepts only the customer-safe contract: AnyAPI branding,
8+
nested USD pricing, `relevance`, and `ranking`.
9+
10+
### Removed
11+
12+
- Removed the temporary 0.3.2 compatibility reader for credit-based catalog
13+
responses and the former search field aliases.
14+
315
## 0.3.2
416

517
### Changed

‎__tests__/discovery.test.ts‎

Lines changed: 85 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,13 @@ import { AnyApiClient } from '../src/api.js';
33
import { formatCatalogPrice } from '../src/format.js';
44
import type { FetchLike } from '../src/types.js';
55

6-
const legacyList = {
7-
apis: [{
8-
slug: 'reddit.search',
9-
category: 'social',
10-
name: 'Reddit Search',
11-
description: 'Search Reddit',
12-
fromCredits: 400,
13-
baseCredits: 5,
14-
perItemCredits: 10,
15-
perItemUnit: 'result',
16-
providers: ['hidden-upstream'],
17-
quotes: [
18-
{ fromCredits: 400, baseCredits: 5, perItemCredits: 10, uptimePct: 99.5, latencyP50Ms: 240, requests: 80 },
19-
{ fromCredits: 500, baseCredits: 10, perItemCredits: 12 },
20-
],
21-
}],
22-
};
23-
24-
const replacementList = {
6+
const catalogResponse = {
257
apis: [{
268
id: 'reddit.search',
279
slug: 'reddit.search',
2810
category: 'social',
2911
name: 'Reddit Search',
12+
description: 'Search Reddit',
3013
provider: 'AnyAPI',
3114
pricing: {
3215
from: { model: 'linear', unit: 'result', baseUsd: 0.00005, perUnitUsd: 0.0001, maxUsd: 0.004 },
@@ -36,52 +19,50 @@ const replacementList = {
3619
pricing: { model: 'linear', unit: 'result', baseUsd: 0.00005, perUnitUsd: 0.0001, maxUsd: 0.004 },
3720
health: { window: '30d', uptimePct: 99.5, latencyP50Ms: 240, requests: 80 },
3821
}],
22+
tryEligible: true,
3923
}],
4024
};
4125

42-
describe('discovery compatibility reader', () => {
43-
it('normalizes the legacy browse response into nested USD pricing', async () => {
44-
const client = clientFor(legacyList);
26+
describe('customer-safe discovery reader', () => {
27+
it('reads browse responses with discriminated nested USD pricing', async () => {
28+
let requested = '';
29+
const client = clientFor(catalogResponse, (url) => { requested = url; });
4530
const response = await client.catalog({ category: 'social' });
4631

47-
expect(response.apis[0]).toMatchObject({
48-
slug: 'reddit.search',
49-
category: 'social',
50-
name: 'Reddit Search',
51-
description: 'Search Reddit',
52-
provider: 'AnyAPI',
53-
pricing: replacementList.apis[0]!.pricing,
54-
});
55-
expect(response.apis[0]!.lanes).toHaveLength(2);
56-
expect(response.apis[0]!.lanes?.[0]).toEqual(replacementList.apis[0]!.lanes?.[0]);
32+
const url = new URL(requested);
33+
expect(url.pathname).toBe('/catalog');
34+
expect(Object.fromEntries(url.searchParams)).toEqual({ category: 'social' });
35+
expect(response).toEqual(catalogResponse);
5736
expect(formatCatalogPrice(response.apis[0]!)).toBe(
5837
'from USD 0.00005 + USD 0.0001/result (max USD 0.0040/request)',
5938
);
60-
expect(JSON.stringify(response)).not.toMatch(/credit|hidden-upstream/i);
39+
expectCustomerSafe(response);
6140
});
6241

63-
it('passes the replacement browse response through the same customer-safe model', async () => {
64-
const client = clientFor(replacementList);
65-
const response = await client.catalog();
66-
67-
expect(response).toEqual(replacementList);
68-
});
69-
70-
it('uses dedicated search and adapts the legacy per-1k result', async () => {
42+
it('uses dedicated ranked search and accepts only relevance and ranking', async () => {
7143
let requested = '';
7244
const client = clientFor({
7345
results: [{
7446
slug: 'amazon.product',
47+
platformId: 'amazon',
7548
name: 'Amazon Product',
49+
description: 'Get product details',
7650
category: 'shopping',
77-
priceUsdPer1k: 5,
78-
score: 0.8,
51+
provider: 'AnyAPI',
52+
pricing: {
53+
from: { model: 'flat', unit: 'request', maxUsd: 0.005 },
54+
failoverMaxUsd: 0.006,
55+
},
56+
relevance: 0.92,
57+
highlightFields: [{ path: 'items[].price', type: 'number' }],
7958
}],
8059
total: 1,
81-
mode: 'usecase',
60+
ranking: 'semantic',
8261
}, (url) => { requested = url; });
8362

84-
const response = await client.search({ query: 'wireless headphones', category: 'shopping', platform: 'amazon', limit: 10 });
63+
const response = await client.search({
64+
query: 'wireless headphones', category: 'shopping', platform: 'amazon', limit: 10,
65+
});
8566

8667
const url = new URL(requested);
8768
expect(url.pathname).toBe('/catalog/search');
@@ -91,109 +72,91 @@ describe('discovery compatibility reader', () => {
9172
platform: 'amazon',
9273
limit: '10',
9374
});
94-
expect(response).toEqual({
95-
results: [{
96-
slug: 'amazon.product',
97-
name: 'Amazon Product',
98-
category: 'shopping',
99-
provider: 'AnyAPI',
100-
pricing: {
101-
from: { model: 'flat', unit: 'request', maxUsd: 0.005 },
102-
failoverMaxUsd: 0.005,
103-
},
104-
relevance: 0.8,
105-
}],
75+
expect(response).toMatchObject({
10676
total: 1,
107-
ranking: 'usecase',
108-
});
109-
});
110-
111-
it('reads replacement ranked search results without legacy fields', async () => {
112-
const client = clientFor({
77+
ranking: 'semantic',
11378
results: [{
11479
slug: 'amazon.product',
115-
name: 'Amazon Product',
11680
provider: 'AnyAPI',
117-
pricing: {
118-
from: { model: 'flat', unit: 'request', maxUsd: 0.005 },
119-
failoverMaxUsd: 0.006,
120-
},
81+
pricing: { from: { model: 'flat', unit: 'request', maxUsd: 0.005 } },
12182
relevance: 0.92,
122-
highlightFields: [{ path: 'items[].price', type: 'number' }],
12383
}],
124-
total: 1,
125-
ranking: 'semantic',
126-
});
127-
128-
const response = await client.search({ query: 'product prices' });
129-
130-
expect(response.ranking).toBe('semantic');
131-
expect(response.results[0]).toMatchObject({
132-
slug: 'amazon.product',
133-
provider: 'AnyAPI',
134-
pricing: { from: { model: 'flat', unit: 'request', maxUsd: 0.005 } },
135-
relevance: 0.92,
136-
highlightFields: [{ path: 'items[].price', type: 'number' }],
13784
});
85+
expectCustomerSafe(response);
13886
});
13987

140-
it('normalizes a legacy describe response before customer sanitization', async () => {
88+
it('reads authenticated detail responses with schemas', async () => {
14189
let authorization = '';
142-
const client = clientFor({
143-
id: 'youtube.comments',
144-
slug: 'youtube.comments',
145-
name: 'YouTube Comments',
146-
description: 'Read comments',
147-
priceCredits: 2000,
148-
fromCredits: 1500,
149-
baseCredits: 0,
150-
perItemCredits: 0,
151-
inputSchema: { type: 'object' },
90+
const body = {
91+
...catalogResponse.apis[0],
92+
inputSchema: { type: 'object', properties: { query: { type: 'string' } } },
15293
outputSchema: { type: 'array' },
153-
provider: 'hidden-upstream',
154-
}, undefined, (init) => {
94+
heavy: true,
95+
};
96+
const client = clientFor(body, undefined, (init) => {
15597
authorization = new Headers(init?.headers).get('Authorization') ?? '';
15698
}, true);
15799

158-
const response = await client.describe('youtube.comments');
100+
const response = await client.describe('reddit.search');
159101

160102
expect(authorization).toBe('Bearer aa_live_test');
161-
expect(response).toEqual({
162-
id: 'youtube.comments',
163-
slug: 'youtube.comments',
164-
name: 'YouTube Comments',
165-
description: 'Read comments',
166-
inputSchema: { type: 'object' },
167-
outputSchema: { type: 'array' },
168-
provider: 'AnyAPI',
169-
pricing: {
170-
from: { model: 'flat', unit: 'request', maxUsd: 0.015 },
171-
failoverMaxUsd: 0.02,
172-
},
173-
});
174-
expect(JSON.stringify(response)).not.toMatch(/credit|hidden-upstream/i);
103+
expect(response).toEqual(body);
104+
expectCustomerSafe(response);
175105
});
176106

177-
it('reads a replacement describe response with schemas and discriminated pricing', async () => {
107+
it('recursively strips forbidden accounting and provider metadata', async () => {
178108
const body = {
179-
id: 'youtube.comments',
180-
slug: 'youtube.comments',
181-
name: 'YouTube Comments',
182-
provider: 'AnyAPI',
183-
pricing: {
184-
from: { model: 'flat', unit: 'request', maxUsd: 0.015 },
185-
failoverMaxUsd: 0.02,
109+
...catalogResponse.apis[0],
110+
provider: 'hidden-upstream',
111+
inputSchema: {
112+
type: 'object',
113+
internalCredits: 500,
114+
provider: 'hidden-upstream',
115+
providers: ['hidden-upstream'],
116+
properties: { query: { type: 'string' } },
186117
},
187-
inputSchema: { type: 'object' },
188-
outputSchema: { type: 'array' },
189-
heavy: true,
190118
};
191119
const client = clientFor(body, undefined, undefined, true);
192120

193-
expect(await client.describe('youtube.comments')).toEqual(body);
121+
const response = await client.describe('reddit.search');
122+
123+
expect(JSON.stringify(response)).not.toContain('hidden-upstream');
124+
expectCustomerSafe(response);
125+
});
126+
127+
it('rejects discovery entries without nested pricing', async () => {
128+
const client = clientFor({
129+
apis: [{
130+
slug: 'reddit.search',
131+
category: 'social',
132+
name: 'Reddit Search',
133+
description: 'Search Reddit',
134+
provider: 'AnyAPI',
135+
}],
136+
});
137+
138+
await expect(client.catalog()).rejects.toThrow('Invalid AnyAPI API discovery response.');
194139
});
195140
});
196141

142+
function expectCustomerSafe(value: unknown): void {
143+
if (Array.isArray(value)) {
144+
value.forEach(expectCustomerSafe);
145+
return;
146+
}
147+
if (typeof value !== 'object' || value === null) {
148+
return;
149+
}
150+
for (const [key, child] of Object.entries(value)) {
151+
expect(key.toLowerCase()).not.toContain('credit');
152+
expect(key.toLowerCase()).not.toBe('providers');
153+
if (key.toLowerCase() === 'provider') {
154+
expect(child).toBe('AnyAPI');
155+
}
156+
expectCustomerSafe(child);
157+
}
158+
}
159+
197160
function clientFor(
198161
body: unknown,
199162
onUrl?: (url: string) => void,

‎package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "anyapi-cli",
3-
"version": "0.3.2",
3+
"version": "0.3.3",
44
"description": "Official CLI for AnyAPI, a unified marketplace for scraping and data APIs.",
55
"type": "module",
66
"bin": {

‎skills/anyapi-discover/SKILL.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ Search and list are public. Describe is authenticated because it returns the ful
3434
- `anyapi search <query>` uses the dedicated ranked discovery search.
3535
- `anyapi list --category <cat>` narrows by category.
3636
- `anyapi describe <sku>` prints input schema, output schema, and USD pricing.
37+
- Discovery pricing is always nested under `pricing`; ranked search reports
38+
`relevance` per result and `ranking` for the response.
3739

3840
## Tips
3941

‎src/api.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { CATALOG_URL, REST_BASE_URL, SIGNUP_URL } from './constants.js';
22
import { readCatalogResponse, readDiscoveryApi, readSearchResponse } from './discovery.js';
33
import { ApiError } from './errors.js';
44
import type {
5+
CatalogApi,
56
CatalogResponse,
67
ClientRegistrationResponse,
78
FetchLike,
@@ -117,11 +118,11 @@ export class AnyApiClient {
117118
return readSearchResponse(body);
118119
}
119120

120-
async describe(sku: string): Promise<unknown> {
121+
async describe(sku: string): Promise<CatalogApi> {
121122
const body = await this.requestJson<unknown>(`${this.restBaseUrl}/apis/${encodeURIComponent(sku)}`, {
122123
headers: this.authHeaders(),
123124
}, { sanitize: false });
124-
return readDiscoveryApi(body) ?? sanitizeCustomerJson(body);
125+
return readDiscoveryApi(body);
125126
}
126127

127128
// run always fetches the FULL result. Response shaping (fields/max_items/summary/

0 commit comments

Comments
 (0)