Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/__tests__/pinecone.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ describe('Pinecone', () => {
} as PineconeConfiguration);
}).toThrow(
'Object contained invalid properties: unknownProp. Valid properties include apiKey, controllerHostUrl,' +
' fetchApi, additionalHeaders, sourceTag, maxRetries, assistantRegion.'
' fetchApi, additionalHeaders, sourceTag, caller, maxRetries, assistantRegion.'
);
});
});
Expand Down
28 changes: 28 additions & 0 deletions src/data/vectors/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,33 @@ export type PineconeConfiguration = {
*/
sourceTag?: string;

/**
* Optional caller information that is applied to the User-Agent header with all requests.
* Used to identify agentic callers using the SDK (e.g., AI coding assistants).
*
* @example
* ```typescript
* const pc = new Pinecone({
* apiKey: 'your-api-key',
* caller: {
* provider: 'google',
* model: 'gemini'
* }
* });
* // User-Agent: ...; caller=google:gemini
* ```
*/
caller?: {
/**
* Optional provider identifier (e.g., 'google', 'anthropic', 'openai').
*/
provider?: string;
/**
* Required model name (e.g., 'gemini', 'claude-code', 'gpt-4').
*/
model: string;
};

/**
* Optional configuration field for specifying the maximum number of retries for a request. Defaults to 3.
*/
Expand All @@ -52,6 +79,7 @@ export const PineconeConfigurationProperties: PineconeConfigurationType[] = [
'fetchApi',
'additionalHeaders',
'sourceTag',
'caller',
'maxRetries',
'assistantRegion',
];
Expand Down
166 changes: 166 additions & 0 deletions src/utils/__tests__/user-agent.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { buildUserAgent } from '../user-agent';
import * as EnvironmentModule from '../environment';
import type { PineconeConfiguration } from '../../data';

describe('user-agent', () => {
describe('buildUserAgent', () => {
Expand All @@ -20,6 +21,40 @@ describe('user-agent', () => {
const userAgent = buildUserAgent(config);
expect(userAgent).toContain('source_tag=test_source_tag');
});

test('applies caller when provided via PineconeConfiguration with provider and model', () => {
const config = {
apiKey: 'test-api-key',
caller: {
provider: 'google',
model: 'gemini',
},
};

const userAgent = buildUserAgent(config);
expect(userAgent).toContain('caller=google:gemini');
});

test('applies caller when provided via PineconeConfiguration with only model', () => {
const config = {
apiKey: 'test-api-key',
caller: {
model: 'claude-code',
},
};

const userAgent = buildUserAgent(config);
expect(userAgent).toContain('caller=claude-code');
});

test('does not include caller when not provided via PineconeConfiguration', () => {
const config = {
apiKey: 'test-api-key',
};

const userAgent = buildUserAgent(config);
expect(userAgent).not.toContain('caller=');
});
});

describe('normalizeSourceTag', () => {
Expand Down Expand Up @@ -48,4 +83,135 @@ describe('user-agent', () => {
expect(userAgent).toContain('source_tag=my_source_tag_:1234abcd');
});
});

describe('caller formatting', () => {
test('normalizes caller strings with special characters', () => {
let config: PineconeConfiguration = {
apiKey: 'test-api-key',
caller: {
provider: 'Google',
model: 'Gemini 2.5',
},
};
let userAgent = buildUserAgent(config);
expect(userAgent).toContain('caller=google:gemini_2.5');

config = {
apiKey: 'test-api-key',
caller: {
provider: ' My Provider ',
model: 'Model-Name!!!',
},
};
userAgent = buildUserAgent(config);
expect(userAgent).toContain('caller=my_provider:model-name');

config = {
apiKey: 'test-api-key',
caller: {
model: 'Claude Code Version',
},
};
userAgent = buildUserAgent(config);
expect(userAgent).toContain('caller=claude_code_version');
});

test('replaces colons with underscores in caller values', () => {
const config = {
apiKey: 'test-api-key',
caller: {
provider: 'open:ai',
model: 'gpt:4',
},
};
const userAgent = buildUserAgent(config);
expect(userAgent).toContain('caller=open_ai:gpt_4');
});

test('replaces colons with underscores in provider and model to prevent parsing ambiguity', () => {
const config = {
apiKey: 'test-api-key',
caller: {
provider: 'open:ai',
model: 'gpt-4',
},
};
const userAgent = buildUserAgent(config);
// Colons should be replaced with underscores in provider, so "open:ai" becomes "open_ai"
expect(userAgent).toContain('caller=open_ai:gpt-4');
expect(userAgent).not.toContain('caller=open:ai:gpt-4');

const config2 = {
apiKey: 'test-api-key',
caller: {
provider: 'google',
model: 'gemini:2.0',
},
};
const userAgent2 = buildUserAgent(config2);
// Colons should be replaced with underscores in model
expect(userAgent2).toContain('caller=google:gemini_2.0');
expect(userAgent2).not.toContain('caller=google:gemini:2.0');
});

test('handles empty or invalid caller values gracefully', () => {
let config: PineconeConfiguration = {
apiKey: 'test-api-key',
caller: {
provider: '',
model: 'valid-model',
},
};
let userAgent = buildUserAgent(config);
expect(userAgent).toContain('caller=valid-model');

config = {
apiKey: 'test-api-key',
caller: {
model: '',
},
};
userAgent = buildUserAgent(config);
expect(userAgent).not.toContain('caller=');

config = {
apiKey: 'test-api-key',
caller: {
provider: ' ',
model: 'valid-model',
},
};
userAgent = buildUserAgent(config);
expect(userAgent).toContain('caller=valid-model');

config = {
apiKey: 'test-api-key',
caller: {
provider: 'valid-provider',
model: '!!!',
},
};
userAgent = buildUserAgent(config);
expect(userAgent).not.toContain('caller=');

config = {
apiKey: 'test-api-key',
caller: {
model: ' ',
},
};
userAgent = buildUserAgent(config);
expect(userAgent).not.toContain('caller=');

config = {
apiKey: 'test-api-key',
caller: {
provider: 'valid-provider',
model: ' ',
},
};
userAgent = buildUserAgent(config);
expect(userAgent).not.toContain('caller=');
});
});
});
51 changes: 51 additions & 0 deletions src/utils/user-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ export const buildUserAgent = (config: PineconeConfiguration) => {
userAgentParts.push(`source_tag=${normalizeSourceTag(config.sourceTag)}`);
}

if (config.caller) {
const callerString = formatCaller(config.caller);
if (callerString) {
userAgentParts.push(`caller=${callerString}`);
}
}

return userAgentParts.join('; ');
};

Expand All @@ -45,3 +52,47 @@ const normalizeSourceTag = (sourceTag: string) => {
.trim()
.replace(/[ ]+/g, '_');
};

const formatCaller = (caller: {
provider?: string;
model: string;
}): string | undefined => {
if (!caller.model) {
return;
}

const normalizedModel = normalizeCallerString(caller.model);
if (!normalizedModel) {
return;
}

if (caller.provider) {
const normalizedProvider = normalizeCallerString(caller.provider);
if (normalizedProvider) {
return `${normalizedProvider}:${normalizedModel}`;
}
}

return normalizedModel;
};

const normalizeCallerString = (str: string): string | undefined => {
if (!str) {
return;
}

/**
* normalize caller string
* 1. Lowercase
* 2. Replace colons with underscores (colons are used as the delimiter between provider and model)
* 3. Limit charset to [a-z0-9_ \-.] (allowing hyphens, periods, and spaces)
* 4. Trim left/right spaces
* 5. Condense multiple spaces to one, and replace with underscore
*/
return str
.toLowerCase()
.replace(/:/g, '_')
.replace(/[^a-z0-9_ \-.]/g, '')
.trim()
.replace(/[ ]+/g, '_');
};
Loading